penumbra_sdk_app/action_handler/
transaction.rs

1use 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    // We only instrument the top-level `check_stateless`, so we get one span for each transaction.
31    #[instrument(skip(self, _context))]
32    async fn check_stateless(&self, _context: ()) -> Result<()> {
33        // This check should be done first, and complete before all other
34        // stateless checks, like proof verification.  In addition to proving
35        // that value balances, the binding signature binds the proofs to the
36        // transaction, as the binding signature can only be created with
37        // knowledge of all of the openings to the commitments the transaction
38        // makes proofs against. (This is where the name binding signature comes
39        // from).
40        //
41        // This allows us to cheaply eliminate a large class of invalid
42        // transactions upfront -- past this point, we can be sure that the user
43        // who submitted the transaction actually formed the proofs, rather than
44        // replaying them from another transaction.
45        valid_binding_signature(self)?;
46        // Other checks probably too cheap to be worth splitting into tasks.
47        num_clues_equal_to_num_outputs(self)?;
48        check_memo_exists_if_outputs_absent_if_not(self)?;
49        // This check ensures that transactions contain at least one action.
50        check_non_empty_transaction(self)?;
51
52        let context = self.context();
53
54        // Currently, we need to clone the component actions so that the spawned
55        // futures can have 'static lifetimes. In the future, we could try to
56        // use the yoke crate, but cloning is almost certainly not a big deal
57        // for now.
58        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        // Now check if any component action failed verification.
66        while let Some(check) = action_checks.join_next().await {
67            check??;
68        }
69
70        Ok(())
71    }
72
73    // We only instrument the top-level `check_stateful`, so we get one span for each transaction.
74    #[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        // SAFETY: Transaction parameters (chain id, expiry height) against chain state
79        // that cannot change during transaction execution.
80        // The fee is _not_ checked here, but during execution.
81        tx_parameters_historical_check(state.clone(), self).await?;
82        // SAFETY: anchors are historical data and cannot change during transaction execution.
83        claimed_anchor_is_valid(state.clone(), self).await?;
84        // SAFETY: FMD parameters cannot change during transaction execution.
85        fmd_parameters_valid(state.clone(), self).await?;
86
87        // Currently, we need to clone the component actions so that the spawned
88        // futures can have 'static lifetimes. In the future, we could try to
89        // use the yoke crate, but cloning is almost certainly not a big deal
90        // for now.
91        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        // Now check if any component action failed verification.
98        while let Some(check) = action_checks.join_next().await {
99            check??;
100        }
101
102        Ok(())
103    }
104
105    // We only instrument the top-level `execute`, so we get one span for each transaction.
106    #[instrument(skip(self, state))]
107    async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
108        // While we have access to the full Transaction, hash it to
109        // obtain a NoteSource we can cache for various actions.
110        state.put_current_source(Some(self.id()));
111
112        // Check and record the transaction's fee payment,
113        // before doing the rest of execution.
114        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        // Delete the note source, in case someone else tries to read it.
127        state.put_current_source(None);
128
129        // Record all the clues in this transaction
130        // To avoid recomputing a hash.
131        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        // Generate two notes controlled by the test address.
166        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        // Record that note in an SCT, where we can generate an auth path.
178        let mut sct = tct::Tree::new();
179        // Assume there's a bunch of stuff already in the SCT.
180        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        // Do we want to seal the SCT block here?
188        let auth_path = sct.witness(note.commit()).unwrap();
189        let auth_path2 = sct.witness(note2.commit()).unwrap();
190
191        // Add a single spend and output to the transaction plan such that the
192        // transaction balances.
193        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        // Build the transaction.
215        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        // On the verifier side, perform stateless verification.
238        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        // Generate a note controlled by the test address.
249        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        // Record that note in an SCT, where we can generate an auth path.
256        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        // Add a single spend and output to the transaction plan such that the
262        // transaction balances.
263        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        // Build the transaction.
278        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        // Set the anchor to the wrong root.
299        tx.anchor = wrong_root;
300
301        // On the verifier side, perform stateless verification.
302        let result = tx.check_stateless(()).await;
303        assert!(result.is_err());
304
305        Ok(())
306    }
307}