penumbra_sdk_app/action_handler/transaction/
stateful.rs

1use anyhow::{ensure, Result};
2use cnidarium::StateRead;
3use penumbra_sdk_sct::component::clock::EpochRead;
4use penumbra_sdk_sct::component::tree::VerificationExt;
5use penumbra_sdk_shielded_pool::component::StateReadExt as _;
6use penumbra_sdk_shielded_pool::fmd;
7use penumbra_sdk_transaction::{Transaction, TransactionParameters};
8
9use crate::app::StateReadExt;
10
11pub async fn tx_parameters_historical_check<S: StateRead>(
12    state: S,
13    transaction: &Transaction,
14) -> Result<()> {
15    let TransactionParameters {
16        chain_id,
17        expiry_height,
18        // This is checked during execution.
19        fee: _,
20        // IMPORTANT: Adding a transaction parameter? Then you **must** add a SAFETY
21        // argument here to justify why it is safe to validate against a historical
22        // state.
23    } = transaction.transaction_parameters();
24
25    // SAFETY: This is safe to do in a **historical** check because the chain's actual
26    // id cannot change during transaction processing.
27    chain_id_is_correct(&state, chain_id).await?;
28    // SAFETY: This is safe to do in a **historical** check because the chain's current
29    // block height cannot change during transaction processing.
30    expiry_height_is_valid(&state, expiry_height).await?;
31
32    Ok(())
33}
34
35pub async fn chain_id_is_correct<S: StateRead>(state: S, tx_chain_id: String) -> Result<()> {
36    let chain_id = state.get_chain_id().await?;
37
38    // The chain ID in the transaction must exactly match the current chain ID.
39    ensure!(
40        tx_chain_id == chain_id,
41        "transaction chain ID '{}' must match the current chain ID '{}'",
42        tx_chain_id,
43        chain_id
44    );
45    Ok(())
46}
47
48pub async fn expiry_height_is_valid<S: StateRead>(state: S, expiry_height: u64) -> Result<()> {
49    let current_height = state.get_block_height().await?;
50
51    // A zero expiry height means that the transaction is valid indefinitely.
52    if expiry_height == 0 {
53        return Ok(());
54    }
55
56    // Otherwise, the expiry height must be greater than or equal to the current block height.
57    ensure!(
58        expiry_height >= current_height,
59        "transaction expiry height '{}' must be greater than or equal to the current block height '{}'",
60        expiry_height,
61        current_height
62    );
63
64    Ok(())
65}
66
67pub async fn fmd_parameters_valid<S: StateRead>(state: S, transaction: &Transaction) -> Result<()> {
68    let meta_params = state
69        .get_shielded_pool_params()
70        .await
71        .expect("chain params request must succeed")
72        .fmd_meta_params;
73    let previous_fmd_parameters = state
74        .get_previous_fmd_parameters()
75        .await
76        .expect("chain params request must succeed");
77    let current_fmd_parameters = state
78        .get_current_fmd_parameters()
79        .await
80        .expect("chain params request must succeed");
81    let height = state.get_block_height().await?;
82    fmd_precision_within_grace_period(
83        transaction,
84        meta_params,
85        previous_fmd_parameters,
86        current_fmd_parameters,
87        height,
88    )
89}
90
91#[tracing::instrument(
92    skip_all,
93    fields(
94        current_fmd.precision_bits = current_fmd_parameters.precision.bits(),
95        previous_fmd.precision_bits = previous_fmd_parameters.precision.bits(),
96        previous_fmd.as_of_block_height = previous_fmd_parameters.as_of_block_height,
97        block_height,
98    )
99)]
100pub fn fmd_precision_within_grace_period(
101    tx: &Transaction,
102    meta_params: fmd::MetaParameters,
103    previous_fmd_parameters: fmd::Parameters,
104    current_fmd_parameters: fmd::Parameters,
105    block_height: u64,
106) -> anyhow::Result<()> {
107    for clue in tx
108        .transaction_body()
109        .detection_data
110        .unwrap_or_default()
111        .fmd_clues
112    {
113        // Clue must be using the current `fmd::Parameters`, or be within
114        // `fmd_grace_period_blocks` of the previous `fmd::Parameters`.
115        let clue_precision = clue.precision()?;
116        let using_current_precision = clue_precision == current_fmd_parameters.precision;
117        let using_previous_precision = clue_precision == previous_fmd_parameters.precision;
118        let within_grace_period = block_height
119            < previous_fmd_parameters.as_of_block_height + meta_params.fmd_grace_period_blocks;
120        if using_current_precision || (using_previous_precision && within_grace_period) {
121            continue;
122        } else {
123            tracing::error!(
124                %clue_precision,
125                %using_current_precision,
126                %using_previous_precision,
127                %within_grace_period,
128                "invalid clue precision"
129            );
130            anyhow::bail!("consensus rule violated: invalid clue precision");
131        }
132    }
133    Ok(())
134}
135
136pub async fn claimed_anchor_is_valid<S: StateRead>(
137    state: S,
138    transaction: &Transaction,
139) -> Result<()> {
140    state.check_claimed_anchor(transaction.anchor).await
141}