penumbra_sdk_app/action_handler/transaction/
stateful.rs1use 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 fee: _,
20 } = transaction.transaction_parameters();
24
25 chain_id_is_correct(&state, chain_id).await?;
28 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 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 if expiry_height == 0 {
53 return Ok(());
54 }
55
56 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 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}