penumbra_sdk_app/action_handler/
transaction.rs1use std::sync::Arc;
2
3use anyhow::Result;
4use async_trait::async_trait;
5use cnidarium::{StateRead, StateWrite};
6use penumbra_sdk_fee::component::FeePay as _;
7use penumbra_sdk_sct::component::source::SourceContext;
8use penumbra_sdk_shielded_pool::component::ClueManager;
9use penumbra_sdk_transaction::{gas::GasCost as _, Transaction};
10use tokio::task::JoinSet;
11use tracing::{instrument, Instrument};
12
13use super::AppActionHandler;
14
15mod stateful;
16mod stateless;
17
18use self::stateful::{
19 claimed_anchor_is_valid, fmd_parameters_valid, tx_parameters_historical_check,
20};
21use stateless::{
22 check_memo_exists_if_outputs_absent_if_not, check_non_empty_transaction,
23 num_clues_equal_to_num_outputs, valid_binding_signature,
24};
25
26#[async_trait]
27impl AppActionHandler for Transaction {
28 type CheckStatelessContext = ();
29
30 #[instrument(skip(self, _context))]
32 async fn check_stateless(&self, _context: ()) -> Result<()> {
33 valid_binding_signature(self)?;
46 num_clues_equal_to_num_outputs(self)?;
48 check_memo_exists_if_outputs_absent_if_not(self)?;
49 check_non_empty_transaction(self)?;
51
52 let context = self.context();
53
54 let mut action_checks = JoinSet::new();
59 for (i, action) in self.actions().cloned().enumerate() {
60 let context2 = context.clone();
61 let span = action.create_span(i);
62 action_checks
63 .spawn(async move { action.check_stateless(context2).await }.instrument(span));
64 }
65 while let Some(check) = action_checks.join_next().await {
67 check??;
68 }
69
70 Ok(())
71 }
72
73 #[instrument(skip(self, state))]
75 async fn check_historical<S: StateRead + 'static>(&self, state: Arc<S>) -> Result<()> {
76 let mut action_checks = JoinSet::new();
77
78 tx_parameters_historical_check(state.clone(), self).await?;
82 claimed_anchor_is_valid(state.clone(), self).await?;
84 fmd_parameters_valid(state.clone(), self).await?;
86
87 for (i, action) in self.actions().cloned().enumerate() {
92 let state2 = state.clone();
93 let span = action.create_span(i);
94 action_checks
95 .spawn(async move { action.check_historical(state2).await }.instrument(span));
96 }
97 while let Some(check) = action_checks.join_next().await {
99 check??;
100 }
101
102 Ok(())
103 }
104
105 #[instrument(skip(self, state))]
107 async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
108 state.put_current_source(Some(self.id()));
111
112 let gas_used = self.gas_cost();
115 let fee = self.transaction_body.transaction_parameters.fee;
116 state.pay_fee(gas_used, fee).await?;
117
118 for (i, action) in self.actions().enumerate() {
119 let span = action.create_span(i);
120 action
121 .check_and_execute(&mut state)
122 .instrument(span)
123 .await?;
124 }
125
126 state.put_current_source(None);
128
129 let id = self.id();
132 for clue in self
133 .transaction_body
134 .detection_data
135 .iter()
136 .flat_map(|x| x.fmd_clues.iter())
137 {
138 state.record_clue(clue.clone(), id.clone()).await?;
139 }
140
141 Ok(())
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use std::ops::Deref;
148
149 use anyhow::Result;
150 use penumbra_sdk_asset::{Value, STAKING_TOKEN_ASSET_ID};
151 use penumbra_sdk_fee::Fee;
152 use penumbra_sdk_keys::test_keys;
153 use penumbra_sdk_shielded_pool::{Note, OutputPlan, SpendPlan};
154 use penumbra_sdk_tct as tct;
155 use penumbra_sdk_transaction::{
156 plan::{CluePlan, DetectionDataPlan, TransactionPlan},
157 TransactionParameters, WitnessData,
158 };
159 use rand_core::OsRng;
160
161 use crate::AppActionHandler;
162
163 #[tokio::test]
164 async fn check_stateless_succeeds_on_valid_spend() -> Result<()> {
165 let value = Value {
167 amount: 100u64.into(),
168 asset_id: *STAKING_TOKEN_ASSET_ID,
169 };
170 let note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
171 let value2 = Value {
172 amount: 50u64.into(),
173 asset_id: *STAKING_TOKEN_ASSET_ID,
174 };
175 let note2 = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value2);
176
177 let mut sct = tct::Tree::new();
179 for _ in 0..5 {
181 let random_note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
182 sct.insert(tct::Witness::Keep, random_note.commit())
183 .unwrap();
184 }
185 sct.insert(tct::Witness::Keep, note.commit()).unwrap();
186 sct.insert(tct::Witness::Keep, note2.commit()).unwrap();
187 let auth_path = sct.witness(note.commit()).unwrap();
189 let auth_path2 = sct.witness(note2.commit()).unwrap();
190
191 let plan = TransactionPlan {
194 transaction_parameters: TransactionParameters {
195 expiry_height: 0,
196 fee: Fee::default(),
197 chain_id: "".into(),
198 },
199 actions: vec![
200 SpendPlan::new(&mut OsRng, note, auth_path.position()).into(),
201 SpendPlan::new(&mut OsRng, note2, auth_path2.position()).into(),
202 OutputPlan::new(&mut OsRng, value, test_keys::ADDRESS_1.deref().clone()).into(),
203 ],
204 detection_data: Some(DetectionDataPlan {
205 clue_plans: vec![CluePlan::new(
206 &mut OsRng,
207 test_keys::ADDRESS_1.deref().clone(),
208 1.try_into().unwrap(),
209 )],
210 }),
211 memo: None,
212 };
213
214 let fvk = &test_keys::FULL_VIEWING_KEY;
216 let sk = &test_keys::SPEND_KEY;
217 let auth_data = plan.authorize(OsRng, sk)?;
218 let witness_data = WitnessData {
219 anchor: sct.root(),
220 state_commitment_proofs: plan
221 .spend_plans()
222 .map(|spend| {
223 (
224 spend.note.commit(),
225 sct.witness(spend.note.commit()).unwrap(),
226 )
227 })
228 .collect(),
229 };
230 let tx = plan
231 .build_concurrent(fvk, &witness_data, &auth_data)
232 .await
233 .expect("can build transaction");
234
235 let context = tx.context();
236
237 for action in tx.transaction_body().actions {
239 let result = action.check_stateless(context.clone()).await;
240 assert!(result.is_ok())
241 }
242
243 Ok(())
244 }
245
246 #[tokio::test]
247 async fn check_stateless_fails_on_auth_path_with_wrong_root() -> Result<()> {
248 let value = Value {
250 amount: 100u64.into(),
251 asset_id: *STAKING_TOKEN_ASSET_ID,
252 };
253 let note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
254
255 let mut sct = tct::Tree::new();
257 let wrong_root = sct.root();
258 sct.insert(tct::Witness::Keep, note.commit()).unwrap();
259 let auth_path = sct.witness(note.commit()).unwrap();
260
261 let plan = TransactionPlan {
264 transaction_parameters: TransactionParameters {
265 expiry_height: 0,
266 fee: Fee::default(),
267 chain_id: "".into(),
268 },
269 actions: vec![
270 SpendPlan::new(&mut OsRng, note, auth_path.position()).into(),
271 OutputPlan::new(&mut OsRng, value, test_keys::ADDRESS_1.deref().clone()).into(),
272 ],
273 detection_data: None,
274 memo: None,
275 };
276
277 let fvk = &test_keys::FULL_VIEWING_KEY;
279 let sk = &test_keys::SPEND_KEY;
280 let auth_data = plan.authorize(OsRng, sk)?;
281 let witness_data = WitnessData {
282 anchor: sct.root(),
283 state_commitment_proofs: plan
284 .spend_plans()
285 .map(|spend| {
286 (
287 spend.note.commit(),
288 sct.witness(spend.note.commit()).unwrap(),
289 )
290 })
291 .collect(),
292 };
293 let mut tx = plan
294 .build_concurrent(fvk, &witness_data, &auth_data)
295 .await
296 .expect("can build transaction");
297
298 tx.anchor = wrong_root;
300
301 let result = tx.check_stateless(()).await;
303 assert!(result.is_err());
304
305 Ok(())
306 }
307}