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, CommitmentSource};
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 let source = CommitmentSource::Transaction {
111 id: Some(self.id().0),
112 };
113 state.put_current_source(Some(source));
114
115 let gas_used = self.gas_cost();
118 let fee = self.transaction_body.transaction_parameters.fee;
119 state.pay_fee(gas_used, fee).await?;
120
121 for (i, action) in self.actions().enumerate() {
122 let span = action.create_span(i);
123 action
124 .check_and_execute(&mut state)
125 .instrument(span)
126 .await?;
127 }
128
129 state.put_current_source(None);
131
132 let id = self.id();
135 for clue in self
136 .transaction_body
137 .detection_data
138 .iter()
139 .flat_map(|x| x.fmd_clues.iter())
140 {
141 state.record_clue(clue.clone(), id.clone()).await?;
142 }
143
144 Ok(())
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use std::ops::Deref;
151
152 use anyhow::Result;
153 use penumbra_sdk_asset::{Value, STAKING_TOKEN_ASSET_ID};
154 use penumbra_sdk_fee::Fee;
155 use penumbra_sdk_keys::test_keys;
156 use penumbra_sdk_shielded_pool::{Note, OutputPlan, SpendPlan};
157 use penumbra_sdk_tct as tct;
158 use penumbra_sdk_transaction::{
159 plan::{CluePlan, DetectionDataPlan, TransactionPlan},
160 TransactionParameters, WitnessData,
161 };
162 use rand_core::OsRng;
163
164 use crate::AppActionHandler;
165
166 #[tokio::test]
167 async fn check_stateless_succeeds_on_valid_spend() -> Result<()> {
168 let value = Value {
170 amount: 100u64.into(),
171 asset_id: *STAKING_TOKEN_ASSET_ID,
172 };
173 let note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
174 let value2 = Value {
175 amount: 50u64.into(),
176 asset_id: *STAKING_TOKEN_ASSET_ID,
177 };
178 let note2 = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value2);
179
180 let mut sct = tct::Tree::new();
182 for _ in 0..5 {
184 let random_note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
185 sct.insert(tct::Witness::Keep, random_note.commit())
186 .unwrap();
187 }
188 sct.insert(tct::Witness::Keep, note.commit()).unwrap();
189 sct.insert(tct::Witness::Keep, note2.commit()).unwrap();
190 let auth_path = sct.witness(note.commit()).unwrap();
192 let auth_path2 = sct.witness(note2.commit()).unwrap();
193
194 let plan = TransactionPlan {
197 transaction_parameters: TransactionParameters {
198 expiry_height: 0,
199 fee: Fee::default(),
200 chain_id: "".into(),
201 },
202 actions: vec![
203 SpendPlan::new(&mut OsRng, note, auth_path.position()).into(),
204 SpendPlan::new(&mut OsRng, note2, auth_path2.position()).into(),
205 OutputPlan::new(&mut OsRng, value, test_keys::ADDRESS_1.deref().clone()).into(),
206 ],
207 detection_data: Some(DetectionDataPlan {
208 clue_plans: vec![CluePlan::new(
209 &mut OsRng,
210 test_keys::ADDRESS_1.deref().clone(),
211 1.try_into().unwrap(),
212 )],
213 }),
214 memo: None,
215 };
216
217 let fvk = &test_keys::FULL_VIEWING_KEY;
219 let sk = &test_keys::SPEND_KEY;
220 let auth_data = plan.authorize(OsRng, sk)?;
221 let witness_data = WitnessData {
222 anchor: sct.root(),
223 state_commitment_proofs: plan
224 .spend_plans()
225 .map(|spend| {
226 (
227 spend.note.commit(),
228 sct.witness(spend.note.commit()).unwrap(),
229 )
230 })
231 .collect(),
232 };
233 let tx = plan
234 .build_concurrent(fvk, &witness_data, &auth_data)
235 .await
236 .expect("can build transaction");
237
238 let context = tx.context();
239
240 for action in tx.transaction_body().actions {
242 let result = action.check_stateless(context.clone()).await;
243 assert!(result.is_ok())
244 }
245
246 Ok(())
247 }
248
249 #[tokio::test]
250 async fn check_stateless_fails_on_auth_path_with_wrong_root() -> Result<()> {
251 let value = Value {
253 amount: 100u64.into(),
254 asset_id: *STAKING_TOKEN_ASSET_ID,
255 };
256 let note = Note::generate(&mut OsRng, &test_keys::ADDRESS_0, value);
257
258 let mut sct = tct::Tree::new();
260 let wrong_root = sct.root();
261 sct.insert(tct::Witness::Keep, note.commit()).unwrap();
262 let auth_path = sct.witness(note.commit()).unwrap();
263
264 let plan = TransactionPlan {
267 transaction_parameters: TransactionParameters {
268 expiry_height: 0,
269 fee: Fee::default(),
270 chain_id: "".into(),
271 },
272 actions: vec![
273 SpendPlan::new(&mut OsRng, note, auth_path.position()).into(),
274 OutputPlan::new(&mut OsRng, value, test_keys::ADDRESS_1.deref().clone()).into(),
275 ],
276 detection_data: None,
277 memo: None,
278 };
279
280 let fvk = &test_keys::FULL_VIEWING_KEY;
282 let sk = &test_keys::SPEND_KEY;
283 let auth_data = plan.authorize(OsRng, sk)?;
284 let witness_data = WitnessData {
285 anchor: sct.root(),
286 state_commitment_proofs: plan
287 .spend_plans()
288 .map(|spend| {
289 (
290 spend.note.commit(),
291 sct.witness(spend.note.commit()).unwrap(),
292 )
293 })
294 .collect(),
295 };
296 let mut tx = plan
297 .build_concurrent(fvk, &witness_data, &auth_data)
298 .await
299 .expect("can build transaction");
300
301 tx.anchor = wrong_root;
303
304 let result = tx.check_stateless(()).await;
306 assert!(result.is_err());
307
308 Ok(())
309 }
310}