penumbra_sdk_dex/component/action_handler/
swap_claim.rs

1use std::sync::Arc;
2
3use anyhow::{Context, Result};
4use async_trait::async_trait;
5use cnidarium_component::ActionHandler;
6use penumbra_sdk_txhash::TransactionContext;
7
8use cnidarium::{StateRead, StateWrite};
9use penumbra_sdk_proof_params::SWAPCLAIM_PROOF_VERIFICATION_KEY;
10use penumbra_sdk_proto::{DomainType as _, StateWriteProto};
11use penumbra_sdk_sct::component::{
12    source::SourceContext,
13    tree::{SctManager, VerificationExt},
14    StateReadExt as _,
15};
16use penumbra_sdk_shielded_pool::component::NoteManager;
17
18use crate::{
19    component::StateReadExt,
20    event,
21    swap_claim::{SwapClaim, SwapClaimProofPublic},
22};
23
24#[async_trait]
25impl ActionHandler for SwapClaim {
26    type CheckStatelessContext = TransactionContext;
27    async fn check_stateless(&self, context: TransactionContext) -> Result<()> {
28        self.proof
29            .verify(
30                &SWAPCLAIM_PROOF_VERIFICATION_KEY,
31                SwapClaimProofPublic {
32                    anchor: context.anchor,
33                    nullifier: self.body.nullifier,
34                    claim_fee: self.body.fee.clone(),
35                    output_data: self.body.output_data,
36                    note_commitment_1: self.body.output_1_commitment,
37                    note_commitment_2: self.body.output_2_commitment,
38                },
39            )
40            .context("a swap claim proof did not verify")?;
41
42        Ok(())
43    }
44
45    async fn check_historical<S: StateRead + 'static>(&self, state: Arc<S>) -> Result<()> {
46        let swap_claim = self;
47
48        // 1. Validate the epoch duration passed in the swap claim matches
49        // what we know.
50        //
51        // SAFETY: this is safe to check here because the epoch duration cannot change during transaction processing.
52        let epoch_duration = state.get_epoch_duration_parameter().await?;
53        let provided_epoch_duration = swap_claim.epoch_duration;
54        if epoch_duration != provided_epoch_duration {
55            anyhow::bail!("provided epoch duration does not match chain epoch duration");
56        }
57
58        // 2. The stateful check *must* validate that the clearing
59        // prices used in the proof are valid.
60        //
61        // SAFETY: this is safe to check here because the historical batch swap
62        // output data will not change.
63        let provided_output_height = swap_claim.body.output_data.height;
64        let provided_trading_pair = swap_claim.body.output_data.trading_pair;
65        let output_data = state
66            .output_data(provided_output_height, provided_trading_pair)
67            .await?
68            // This check also ensures that the height for the swap is in the past, otherwise
69            // the output data would not be present in the JMT.
70            .ok_or_else(|| anyhow::anyhow!("output data not found"))?;
71
72        if output_data != swap_claim.body.output_data {
73            anyhow::bail!("provided output data does not match chain output data");
74        }
75
76        Ok(())
77    }
78
79    async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
80        // 3. Check that the nullifier hasn't been spent before.
81        let spent_nullifier = self.body.nullifier;
82        state.check_nullifier_unspent(spent_nullifier).await?;
83
84        // Record the output notes in the state.
85        let source = state
86            .get_current_source()
87            .expect("source is set during tx execution");
88
89        state
90            .add_rolled_up_payload(self.body.output_1_commitment, source.clone())
91            .await;
92        state
93            .add_rolled_up_payload(self.body.output_2_commitment, source.clone())
94            .await;
95
96        state.nullify(self.body.nullifier, source).await;
97
98        state.record_proto(event::EventSwapClaim::from(self).to_proto());
99
100        Ok(())
101    }
102}