penumbra_app/action_handler/transaction/
stateful.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use anyhow::{ensure, Result};
use cnidarium::StateRead;
use penumbra_sct::component::clock::EpochRead;
use penumbra_sct::component::tree::VerificationExt;
use penumbra_shielded_pool::component::StateReadExt as _;
use penumbra_shielded_pool::fmd;
use penumbra_transaction::{Transaction, TransactionParameters};

use crate::app::StateReadExt;

pub async fn tx_parameters_historical_check<S: StateRead>(
    state: S,
    transaction: &Transaction,
) -> Result<()> {
    let TransactionParameters {
        chain_id,
        expiry_height,
        // This is checked during execution.
        fee: _,
        // IMPORTANT: Adding a transaction parameter? Then you **must** add a SAFETY
        // argument here to justify why it is safe to validate against a historical
        // state.
    } = transaction.transaction_parameters();

    // SAFETY: This is safe to do in a **historical** check because the chain's actual
    // id cannot change during transaction processing.
    chain_id_is_correct(&state, chain_id).await?;
    // SAFETY: This is safe to do in a **historical** check because the chain's current
    // block height cannot change during transaction processing.
    expiry_height_is_valid(&state, expiry_height).await?;

    Ok(())
}

pub async fn chain_id_is_correct<S: StateRead>(state: S, tx_chain_id: String) -> Result<()> {
    let chain_id = state.get_chain_id().await?;

    // The chain ID in the transaction must exactly match the current chain ID.
    ensure!(
        tx_chain_id == chain_id,
        "transaction chain ID '{}' must match the current chain ID '{}'",
        tx_chain_id,
        chain_id
    );
    Ok(())
}

pub async fn expiry_height_is_valid<S: StateRead>(state: S, expiry_height: u64) -> Result<()> {
    let current_height = state.get_block_height().await?;

    // A zero expiry height means that the transaction is valid indefinitely.
    if expiry_height == 0 {
        return Ok(());
    }

    // Otherwise, the expiry height must be greater than or equal to the current block height.
    ensure!(
        expiry_height >= current_height,
        "transaction expiry height '{}' must be greater than or equal to the current block height '{}'",
        expiry_height,
        current_height
    );

    Ok(())
}

pub async fn fmd_parameters_valid<S: StateRead>(state: S, transaction: &Transaction) -> Result<()> {
    let meta_params = state
        .get_shielded_pool_params()
        .await
        .expect("chain params request must succeed")
        .fmd_meta_params;
    let previous_fmd_parameters = state
        .get_previous_fmd_parameters()
        .await
        .expect("chain params request must succeed");
    let current_fmd_parameters = state
        .get_current_fmd_parameters()
        .await
        .expect("chain params request must succeed");
    let height = state.get_block_height().await?;
    fmd_precision_within_grace_period(
        transaction,
        meta_params,
        previous_fmd_parameters,
        current_fmd_parameters,
        height,
    )
}

#[tracing::instrument(
    skip_all,
    fields(
        current_fmd.precision_bits = current_fmd_parameters.precision.bits(),
        previous_fmd.precision_bits = previous_fmd_parameters.precision.bits(),
        previous_fmd.as_of_block_height = previous_fmd_parameters.as_of_block_height,
        block_height,
    )
)]
pub fn fmd_precision_within_grace_period(
    tx: &Transaction,
    meta_params: fmd::MetaParameters,
    previous_fmd_parameters: fmd::Parameters,
    current_fmd_parameters: fmd::Parameters,
    block_height: u64,
) -> anyhow::Result<()> {
    for clue in tx
        .transaction_body()
        .detection_data
        .unwrap_or_default()
        .fmd_clues
    {
        // Clue must be using the current `fmd::Parameters`, or be within
        // `fmd_grace_period_blocks` of the previous `fmd::Parameters`.
        let clue_precision = clue.precision()?;
        let using_current_precision = clue_precision == current_fmd_parameters.precision;
        let using_previous_precision = clue_precision == previous_fmd_parameters.precision;
        let within_grace_period = block_height
            < previous_fmd_parameters.as_of_block_height + meta_params.fmd_grace_period_blocks;
        if using_current_precision || (using_previous_precision && within_grace_period) {
            continue;
        } else {
            tracing::error!(
                %clue_precision,
                %using_current_precision,
                %using_previous_precision,
                %within_grace_period,
                "invalid clue precision"
            );
            anyhow::bail!("consensus rule violated: invalid clue precision");
        }
    }
    Ok(())
}

pub async fn claimed_anchor_is_valid<S: StateRead>(
    state: S,
    transaction: &Transaction,
) -> Result<()> {
    state.check_claimed_anchor(transaction.anchor).await
}