diff --git a/crates/core/app/src/action_handler/transaction.rs b/crates/core/app/src/action_handler/transaction.rs index 9219a8350b..5e5db3fcf0 100644 --- a/crates/core/app/src/action_handler/transaction.rs +++ b/crates/core/app/src/action_handler/transaction.rs @@ -13,7 +13,10 @@ use super::AppActionHandler; mod stateful; mod stateless; -use self::stateful::{claimed_anchor_is_valid, fee_greater_than_base_fee, fmd_parameters_valid}; +use self::stateful::{ + chain_id_is_correct, claimed_anchor_is_valid, expiry_height_is_valid, + fee_greater_than_base_fee, fmd_parameters_valid, +}; use stateless::{ check_memo_exists_if_outputs_absent_if_not, num_clues_equal_to_num_outputs, valid_binding_signature, @@ -58,6 +61,10 @@ impl AppActionHandler for Transaction { // TODO: these could be pushed into the action checks and run concurrently if needed + // SAFETY: chain ID never changes during a transaction execution. + chain_id_is_correct(state.clone(), self).await?; + // SAFETY: height never changes during a transaction execution. + expiry_height_is_valid(state.clone(), self).await?; // SAFETY: anchors are historical data and cannot change during transaction execution. claimed_anchor_is_valid(state.clone(), self).await?; // SAFETY: FMD parameters cannot change during transaction execution. diff --git a/crates/core/app/src/action_handler/transaction/stateful.rs b/crates/core/app/src/action_handler/transaction/stateful.rs index 7241d1a6a6..ea5a490b86 100644 --- a/crates/core/app/src/action_handler/transaction/stateful.rs +++ b/crates/core/app/src/action_handler/transaction/stateful.rs @@ -8,8 +8,50 @@ use penumbra_shielded_pool::fmd; use penumbra_transaction::gas::GasCost; use penumbra_transaction::Transaction; +use crate::app::StateReadExt; + const FMD_GRACE_PERIOD_BLOCKS: u64 = 10; +pub async fn chain_id_is_correct(state: S, transaction: &Transaction) -> Result<()> { + let chain_id = state.get_chain_id().await?; + let tx_chain_id = &transaction.transaction_body.transaction_parameters.chain_id; + + // 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( + state: S, + transaction: &Transaction, +) -> Result<()> { + let current_height = state.get_block_height().await?; + let expiry_height = transaction + .transaction_body + .transaction_parameters + .expiry_height; + + // 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(state: S, transaction: &Transaction) -> Result<()> { let previous_fmd_parameters = state .get_previous_fmd_parameters()