From edfa418430e0a8341838cd6ca69afc5c346a0761 Mon Sep 17 00:00:00 2001 From: satan Date: Tue, 13 Feb 2024 15:53:44 +0100 Subject: [PATCH 01/17] First compiling version --- Cargo.lock | 7 + crates/apps/Cargo.toml | 1 + crates/apps/src/lib/bench_utils.rs | 1 + crates/apps/src/lib/client/rpc.rs | 31 +- .../src/lib/node/ledger/shell/block_alloc.rs | 260 +++++----- .../node/ledger/shell/block_alloc/states.rs | 65 +-- .../shell/block_alloc/states/encrypted_txs.rs | 127 ----- .../{decrypted_txs.rs => normal_txs.rs} | 27 +- .../shell/block_alloc/states/protocol_txs.rs | 35 +- .../lib/node/ledger/shell/finalize_block.rs | 477 +++--------------- .../src/lib/node/ledger/shell/governance.rs | 2 +- crates/apps/src/lib/node/ledger/shell/mod.rs | 147 +----- .../lib/node/ledger/shell/prepare_proposal.rs | 263 ++-------- .../lib/node/ledger/shell/process_proposal.rs | 446 +--------------- .../src/lib/node/ledger/shell/testing/node.rs | 23 +- .../lib/node/ledger/shell/vote_extensions.rs | 18 +- .../src/lib/node/ledger/shims/abcipp_shim.rs | 2 +- .../src/lib/node/ledger/storage/rocksdb.rs | 16 - crates/benches/process_wrapper.rs | 3 - crates/core/src/storage.rs | 3 + crates/namada/src/ledger/native_vp/masp.rs | 9 +- crates/namada/src/ledger/protocol/mod.rs | 37 +- crates/sdk/src/events/log.rs | 18 +- crates/sdk/src/events/log/dumb_queries.rs | 14 +- crates/sdk/src/masp.rs | 258 +++++----- crates/sdk/src/queries/shell.rs | 20 - crates/sdk/src/rpc.rs | 28 +- crates/sdk/src/tx.rs | 78 +-- crates/shielded_token/src/utils.rs | 1 + crates/state/src/lib.rs | 1 - crates/storage/src/db.rs | 5 - crates/storage/src/mockdb.rs | 10 - crates/tx/src/data/decrypted.rs | 1 - crates/tx/src/data/mod.rs | 2 + 34 files changed, 560 insertions(+), 1876 deletions(-) delete mode 100644 crates/apps/src/lib/node/ledger/shell/block_alloc/states/encrypted_txs.rs rename crates/apps/src/lib/node/ledger/shell/block_alloc/states/{decrypted_txs.rs => normal_txs.rs} (51%) diff --git a/Cargo.lock b/Cargo.lock index efd89f312d..5e6aa7b3fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1632,6 +1632,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + [[package]] name = "dtoa" version = "0.4.8" @@ -4221,6 +4227,7 @@ dependencies = [ "data-encoding", "derivative", "directories", + "drain_filter_polyfill", "ed25519-consensus 1.2.1", "ethabi", "ethbridge-bridge-events", diff --git a/crates/apps/Cargo.toml b/crates/apps/Cargo.toml index 72b1161e6a..c9e7fe0135 100644 --- a/crates/apps/Cargo.toml +++ b/crates/apps/Cargo.toml @@ -84,6 +84,7 @@ config.workspace = true data-encoding.workspace = true derivative.workspace = true directories.workspace = true +drain_filter_polyfill = "0.1.3" ed25519-consensus = { workspace = true, features = ["std"] } ethabi.workspace = true ethbridge-bridge-events.workspace = true diff --git a/crates/apps/src/lib/bench_utils.rs b/crates/apps/src/lib/bench_utils.rs index 7b886eacdb..b6ff9229bc 100644 --- a/crates/apps/src/lib/bench_utils.rs +++ b/crates/apps/src/lib/bench_utils.rs @@ -857,6 +857,7 @@ impl Client for BenchShell { .map(|(idx, (_tx, changed_keys))| { let tx_result = TxResult { gas_used: 0.into(), + wrapper_changed_keys: Default::default(), changed_keys: changed_keys.to_owned(), vps_result: VpsResult::default(), initialized_accounts: vec![], diff --git a/crates/apps/src/lib/client/rpc.rs b/crates/apps/src/lib/client/rpc.rs index 52f2c56ba4..3ada4ceb84 100644 --- a/crates/apps/src/lib/client/rpc.rs +++ b/crates/apps/src/lib/client/rpc.rs @@ -58,7 +58,7 @@ use namada_sdk::rpc::{ self, enriched_bonds_and_unbonds, query_epoch, TxResponse, }; use namada_sdk::tendermint_rpc::endpoint::status; -use namada_sdk::tx::{display_inner_resp, display_wrapper_resp_and_get_result}; +use namada_sdk::tx::display_inner_resp; use namada_sdk::wallet::AddressVpType; use namada_sdk::{display, display_line, edisplay_line, error, prompt, Namada}; use tokio::time::Instant; @@ -201,8 +201,12 @@ pub async fn query_transfers( .map(|fvk| (ExtendedFullViewingKey::from(*fvk).fvk.vk, fvk)) .collect(); // Now display historical shielded and transparent transactions - for (IndexedTx { height, index: idx }, (epoch, tfer_delta, tx_delta)) in - transfers + for ( + IndexedTx { + height, index: idx, .. + }, + (epoch, tfer_delta, tx_delta), + ) in transfers { // Check if this transfer pertains to the supplied owner let mut relevant = match &query_owner { @@ -2769,23 +2773,10 @@ pub async fn query_result(context: &impl Namada, args: args::QueryResult) { Ok(resp) => { display_inner_resp(context, &resp); } - Err(err1) => { - // If this fails then instead look for an acceptance event. - let wrapper_resp = query_tx_response( - context.client(), - namada_sdk::rpc::TxEventQuery::Accepted(&args.tx_hash), - ) - .await; - match wrapper_resp { - Ok(resp) => { - display_wrapper_resp_and_get_result(context, &resp); - } - Err(err2) => { - // Print the errors that caused the lookups to fail - edisplay_line!(context.io(), "{}\n{}", err1, err2); - cli::safe_exit(1) - } - } + Err(err) => { + // Print the errors that caused the lookups to fail + edisplay_line!(context.io(), "{}", err); + cli::safe_exit(1) } } } diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index 09bb6d7847..a8ba599f1f 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -16,22 +16,17 @@ //! In the current implementation, we allocate space for transactions //! in the following order of preference: //! -//! - First, we allot space for DKG encrypted txs. We allow DKG encrypted txs to -//! take up at most 1/3 of the total block space. -//! - Next, we allot space for DKG decrypted txs. Decrypted txs take up as much -//! space as needed. We will see, shortly, why in practice this is fine. -//! - Finally, we allot space for protocol txs. Protocol txs get half of the -//! remaining block space allotted to them. +//! - First, we allot space for protocol txs. We allow them to take up at most +//! 1/2 of the total block space unless there is extra room due to a lack of +//! user txs. +//! - Next, we allot space for user submitted txs until the block is filled. +//! - If we cannot fill the block with normal txs, we try to fill it with +//! protocol txs that were not allocated in the initial phase. //! -//! Since at some fixed height `H` decrypted txs only take up as -//! much space as the encrypted txs from height `H - 1`, and we -//! restrict the space of encrypted txs to at most 1/3 of the -//! total block space, we roughly divide the Tendermint block -//! space in 3, for each major type of tx. //! //! # How gas is allocated //! -//! Gas is only relevant to DKG encrypted txs. Every encrypted tx defines its +//! Gas is only relevant to non-protocol txs. Every such tx defines its //! gas limit. We take this entire gas limit as the amount of gas requested by //! the tx. @@ -48,11 +43,6 @@ pub mod states; // and alloc space for large tx right at the start. the problem with // this is that then we may not have enough space for decrypted txs -// TODO: panic if we don't have enough space reserved for a -// decrypted tx; in theory, we should always have enough space -// reserved for decrypted txs, given the invariants of the state -// machine - use std::marker::PhantomData; use namada::proof_of_stake::pos_queries::PosQueries; @@ -60,6 +50,7 @@ use namada::state::{self, WlState}; #[allow(unused_imports)] use crate::facade::tendermint_proto::abci::RequestPrepareProposal; +use crate::node::ledger::shell::block_alloc::states::WithNormalTxs; /// Block allocation failure status responses. #[derive(Debug, Copy, Clone, Eq, PartialEq)] @@ -121,11 +112,10 @@ impl Resource for BlockGas { /// /// We keep track of the current space utilized by: /// -/// - DKG encrypted transactions. -/// - DKG decrypted transactions. +/// - normal transactions. /// - Protocol transactions. /// -/// Gas usage of DKG encrypted txs is also tracked. +/// Gas usage of normal txs is also tracked. #[derive(Debug, Default)] pub struct BlockAllocator { /// The current state of the [`BlockAllocator`] state machine. @@ -135,14 +125,12 @@ pub struct BlockAllocator { block: TxBin, /// The current space utilized by protocol transactions. protocol_txs: TxBin, - /// The current space and gas utilized by DKG encrypted transactions. - encrypted_txs: EncryptedTxsBins, - /// The current space utilized by DKG decrypted transactions. - decrypted_txs: TxBin, + /// The current space and gas utilized by normal user transactions. + normal_txs: NormalTxsBins, } -impl From<&WlState> - for BlockAllocator> +impl From<&WlState> + for BlockAllocator> where D: 'static + state::DB + for<'iter> state::DBIter<'iter>, H: 'static + state::StorageHasher, @@ -156,7 +144,29 @@ where } } -impl BlockAllocator> { +impl BlockAllocator> { + /// Construct a new [`BlockAllocator`], with an upper bound + /// on the max size of all txs in a block defined by Tendermint and an upper + /// bound on the max gas in a block. + #[inline] + pub fn init( + tendermint_max_block_space_in_bytes: u64, + max_block_gas: u64, + ) -> Self { + let max = tendermint_max_block_space_in_bytes; + Self { + _state: PhantomData, + block: TxBin::init(max), + protocol_txs: { + let allotted_space_in_bytes = threshold::ONE_HALF.over(max); + TxBin::init(allotted_space_in_bytes) + }, + normal_txs: NormalTxsBins::new(max_block_gas), + } + } +} + +impl BlockAllocator { /// Construct a new [`BlockAllocator`], with an upper bound /// on the max size of all txs in a block defined by Tendermint and an upper /// bound on the max gas in a block. @@ -170,8 +180,7 @@ impl BlockAllocator> { _state: PhantomData, block: TxBin::init(max), protocol_txs: TxBin::default(), - encrypted_txs: EncryptedTxsBins::new(max, max_block_gas), - decrypted_txs: TxBin::default(), + normal_txs: NormalTxsBins::new(max_block_gas), } } } @@ -185,9 +194,8 @@ impl BlockAllocator { /// to each [`TxBin`] instance in a [`BlockAllocator`]. #[inline] fn uninitialized_space_in_bytes(&self) -> u64 { - let total_bin_space = self.protocol_txs.allotted - + self.encrypted_txs.space.allotted - + self.decrypted_txs.allotted; + let total_bin_space = + self.protocol_txs.allotted + self.normal_txs.space.allotted; self.block.allotted - total_bin_space } } @@ -256,16 +264,15 @@ impl TxBin { } #[derive(Debug, Default)] -pub struct EncryptedTxsBins { +pub struct NormalTxsBins { space: TxBin, gas: TxBin, } -impl EncryptedTxsBins { - pub fn new(max_bytes: u64, max_gas: u64) -> Self { - let allotted_space_in_bytes = threshold::ONE_THIRD.over(max_bytes); +impl NormalTxsBins { + pub fn new(max_gas: u64) -> Self { Self { - space: TxBin::init(allotted_space_in_bytes), + space: TxBin::default(), gas: TxBin::init(max_gas), } } @@ -273,10 +280,10 @@ impl EncryptedTxsBins { pub fn try_dump(&mut self, tx: &[u8], gas: u64) -> Result<(), String> { self.space.try_dump(tx).map_err(|e| match e { AllocFailure::Rejected { .. } => { - "No more space left in the block for wrapper txs".to_string() + "No more space left in the block for normal txs".to_string() } AllocFailure::OverflowsBin { .. } => "The given wrapper tx is \ - larger than 1/3 of the \ + larger than the remaining \ available block space" .to_string(), })?; @@ -317,7 +324,7 @@ pub mod threshold { } /// Divide free space in three. - pub const ONE_THIRD: Threshold = Threshold::new(1, 3); + pub const ONE_HALF: Threshold = Threshold::new(1, 2); } #[cfg(test)] @@ -328,21 +335,19 @@ mod tests { use proptest::prelude::*; use super::states::{ - BuildingEncryptedTxBatch, NextState, TryAlloc, WithEncryptedTxs, - WithoutEncryptedTxs, + BuildingProtocolTxBatch, BuildingTxBatch, NextState, TryAlloc, }; use super::*; use crate::node::ledger::shims::abcipp_shim_types::shim::TxBytes; - /// Convenience alias for a block space allocator at a state with encrypted + /// Convenience alias for a block space allocator at a state with protocol /// txs. - type BsaWrapperTxs = - BlockAllocator>; + type BsaInitialProtocolTxs = + BlockAllocator>; - /// Convenience alias for a block space allocator at a state without - /// encrypted txs. - type BsaNoWrapperTxs = - BlockAllocator>; + /// Convenience alias for a block space allocator at a state with protocol + /// txs. + type BsaNormalTxs = BlockAllocator; /// Proptest generated txs. #[derive(Debug)] @@ -350,45 +355,46 @@ mod tests { tendermint_max_block_space_in_bytes: u64, max_block_gas: u64, protocol_txs: Vec, - encrypted_txs: Vec, - decrypted_txs: Vec, + normal_txs: Vec, } - /// Check that at most 1/3 of the block space is + /// Check that at most 1/2 of the block space is /// reserved for each kind of tx type, in the - /// allocator's common path. + /// allocator's common path. Further check that + /// if not enough normal txs are present, the rest + /// if filled with protocol txs #[test] - fn test_txs_are_evenly_split_across_block() { + fn test_filling_up_with_protocol() { const BLOCK_SIZE: u64 = 60; const BLOCK_GAS: u64 = 1_000; - // reserve block space for encrypted txs - let mut alloc = BsaWrapperTxs::init(BLOCK_SIZE, BLOCK_GAS); + // reserve block space for protocol txs + let mut alloc = BsaInitialProtocolTxs::init(BLOCK_SIZE, BLOCK_GAS); - // allocate ~1/3 of the block space to encrypted txs - assert!(alloc.try_alloc(BlockResources::new(&[0; 18], 0)).is_ok()); + // allocate ~1/2 of the block space to encrypted txs + assert!(alloc.try_alloc(&[0; 29]).is_ok()); - // reserve block space for decrypted txs + // reserve block space for normal txs let mut alloc = alloc.next_state(); - // the space we allotted to encrypted txs was shrunk to + // the space we allotted to protocol txs was shrunk to // the total space we actually used up - assert_eq!(alloc.encrypted_txs.space.allotted, 18); + assert_eq!(alloc.protocol_txs.allotted, 29); - // check that the allotted space for decrypted txs is correct - assert_eq!(alloc.decrypted_txs.allotted, BLOCK_SIZE - 18); + // check that the allotted space for normal txs is correct + assert_eq!(alloc.normal_txs.space.allotted, BLOCK_SIZE - 29); - // add about ~1/3 worth of decrypted txs - assert!(alloc.try_alloc(&[0; 17]).is_ok()); + // add about ~1/3 worth of normal txs + assert!(alloc.try_alloc(BlockResources::new(&[0; 17], 0)).is_ok()); - // reserve block space for protocol txs + // fill the rest of the block with protocol txs let mut alloc = alloc.next_state(); // check that space was shrunk - assert_eq!(alloc.protocol_txs.allotted, BLOCK_SIZE - (18 + 17)); + assert_eq!(alloc.protocol_txs.allotted, BLOCK_SIZE - (29 + 17)); // add protocol txs to the block space allocator - assert!(alloc.try_alloc(&[0; 25]).is_ok()); + assert!(alloc.try_alloc(&[0; 14]).is_ok()); // the block should be full at this point assert_matches!( @@ -397,11 +403,32 @@ mod tests { ); } - // Test that we cannot include encrypted txs in a block - // when the state invariants banish them from inclusion. + /// Test that if less than half of the block can be initially filled + /// with protocol txs, the rest if filled with normal txs. #[test] - fn test_encrypted_txs_are_rejected() { - let mut alloc = BsaNoWrapperTxs::init(1234, 1_000); + fn test_less_than_half_protocol() { + const BLOCK_SIZE: u64 = 60; + const BLOCK_GAS: u64 = 1_000; + + // reserve block space for protocol txs + let mut alloc = BsaInitialProtocolTxs::init(BLOCK_SIZE, BLOCK_GAS); + + // allocate ~1/3 of the block space to encrypted txs + assert!(alloc.try_alloc(&[0; 18]).is_ok()); + + // reserve block space for normal txs + let mut alloc = alloc.next_state(); + + // the space we allotted to protocol txs was shrunk to + // the total space we actually used up + assert_eq!(alloc.protocol_txs.allotted, 18); + + // check that the allotted space for normal txs is correct + assert_eq!(alloc.normal_txs.space.allotted, BLOCK_SIZE - 18); + + // add about ~2/3 worth of normal txs + assert!(alloc.try_alloc(BlockResources::new(&[0; 42], 0)).is_ok()); + // the block should be full at this point assert_matches!( alloc.try_alloc(BlockResources::new(&[0; 1], 0)), Err(AllocFailure::Rejected { .. }) @@ -436,21 +463,21 @@ mod tests { tendermint_max_block_space_in_bytes: u64, ) { let mut bins = - BsaWrapperTxs::init(tendermint_max_block_space_in_bytes, 1_000); + BsaNormalTxs::init(tendermint_max_block_space_in_bytes, 1_000); - // fill the entire bin of encrypted txs - bins.encrypted_txs.space.occupied = bins.encrypted_txs.space.allotted; + // fill the entire bin of protocol txs + bins.normal_txs.space.occupied = bins.normal_txs.space.allotted; - // make sure we can't dump any new encrypted txs in the bin + // make sure we can't dump any new protocol txs in the bin assert_matches!( bins.try_alloc(BlockResources::new(b"arbitrary tx bytes", 0)), Err(AllocFailure::Rejected { .. }) ); // Reset space bin - bins.encrypted_txs.space.occupied = 0; + bins.normal_txs.space.occupied = 0; // Fill the entire gas bin - bins.encrypted_txs.gas.occupied = bins.encrypted_txs.gas.allotted; + bins.normal_txs.gas.occupied = bins.normal_txs.gas.allotted; // Make sure we can't dump any new wncrypted txs in the bin assert_matches!( @@ -461,10 +488,12 @@ mod tests { /// Implementation of [`test_initial_bin_capacity`]. fn proptest_initial_bin_capacity(tendermint_max_block_space_in_bytes: u64) { - let bins = - BsaWrapperTxs::init(tendermint_max_block_space_in_bytes, 1_000); + let bins = BsaInitialProtocolTxs::init( + tendermint_max_block_space_in_bytes, + 1_000, + ); let expected = tendermint_max_block_space_in_bytes - - threshold::ONE_THIRD.over(tendermint_max_block_space_in_bytes); + - threshold::ONE_HALF.over(tendermint_max_block_space_in_bytes); assert_eq!(expected, bins.uninitialized_space_in_bytes()); } @@ -474,8 +503,7 @@ mod tests { tendermint_max_block_space_in_bytes, max_block_gas, protocol_txs, - encrypted_txs, - decrypted_txs, + normal_txs, } = args; // produce new txs until the moment we would have @@ -484,16 +512,32 @@ mod tests { // iterate over the produced txs to make sure we can keep // dumping new txs without filling up the bins - let bins = RefCell::new(BsaWrapperTxs::init( + let bins = RefCell::new(BsaInitialProtocolTxs::init( tendermint_max_block_space_in_bytes, max_block_gas, )); - let encrypted_txs = encrypted_txs.into_iter().take_while(|tx| { - let bin = bins.borrow().encrypted_txs.space; + let mut protocol_tx_iter = protocol_txs.iter(); + let mut allocated_txs = vec![]; + for tx in protocol_tx_iter.by_ref() { + let bin = bins.borrow().protocol_txs; + let new_size = bin.occupied + tx.len() as u64; + if new_size >= bin.allotted { + break; + } else { + allocated_txs.push(tx); + } + } + for tx in allocated_txs { + assert!(bins.borrow_mut().try_alloc(tx).is_ok()); + } + + let bins = RefCell::new(bins.into_inner().next_state()); + let decrypted_txs = normal_txs.into_iter().take_while(|tx| { + let bin = bins.borrow().normal_txs.space; let new_size = bin.occupied + tx.len() as u64; new_size < bin.allotted }); - for tx in encrypted_txs { + for tx in decrypted_txs { assert!( bins.borrow_mut() .try_alloc(BlockResources::new(&tx, 0)) @@ -502,23 +546,19 @@ mod tests { } let bins = RefCell::new(bins.into_inner().next_state()); - let decrypted_txs = decrypted_txs.into_iter().take_while(|tx| { - let bin = bins.borrow().decrypted_txs; + let mut allocated_txs = vec![]; + for tx in protocol_tx_iter.by_ref() { + let bin = bins.borrow().protocol_txs; let new_size = bin.occupied + tx.len() as u64; - new_size < bin.allotted - }); - for tx in decrypted_txs { - assert!(bins.borrow_mut().try_alloc(&tx).is_ok()); + if new_size >= bin.allotted { + break; + } else { + allocated_txs.push(tx); + } } - let bins = RefCell::new(bins.into_inner().next_state()); - let protocol_txs = protocol_txs.into_iter().take_while(|tx| { - let bin = bins.borrow().protocol_txs; - let new_size = bin.occupied + tx.len() as u64; - new_size < bin.allotted - }); - for tx in protocol_txs { - assert!(bins.borrow_mut().try_alloc(&tx).is_ok()); + for tx in allocated_txs { + assert!(bins.borrow_mut().try_alloc(tx).is_ok()); } } @@ -527,7 +567,7 @@ mod tests { fn arb_transactions() // create base strategies ( - (tendermint_max_block_space_in_bytes, max_block_gas, protocol_tx_max_bin_size, encrypted_tx_max_bin_size, + (tendermint_max_block_space_in_bytes, max_block_gas, protocol_tx_max_bin_size, decrypted_tx_max_bin_size) in arb_max_bin_sizes(), ) // compose strategies @@ -535,36 +575,30 @@ mod tests { tendermint_max_block_space_in_bytes in Just(tendermint_max_block_space_in_bytes), max_block_gas in Just(max_block_gas), protocol_txs in arb_tx_list(protocol_tx_max_bin_size), - encrypted_txs in arb_tx_list(encrypted_tx_max_bin_size), - decrypted_txs in arb_tx_list(decrypted_tx_max_bin_size), + normal_txs in arb_tx_list(decrypted_tx_max_bin_size), ) -> PropTx { PropTx { tendermint_max_block_space_in_bytes, max_block_gas, protocol_txs: protocol_txs.into_iter().map(prost::bytes::Bytes::from).collect(), - encrypted_txs: encrypted_txs.into_iter().map(prost::bytes::Bytes::from).collect(), - decrypted_txs: decrypted_txs.into_iter().map(prost::bytes::Bytes::from).collect(), + normal_txs: normal_txs.into_iter().map(prost::bytes::Bytes::from).collect(), } } } /// Return random bin sizes for a [`BlockAllocator`]. - fn arb_max_bin_sizes() - -> impl Strategy { + fn arb_max_bin_sizes() -> impl Strategy { const MAX_BLOCK_SIZE_BYTES: u64 = 1000; (1..=MAX_BLOCK_SIZE_BYTES).prop_map( |tendermint_max_block_space_in_bytes| { ( tendermint_max_block_space_in_bytes, tendermint_max_block_space_in_bytes, - threshold::ONE_THIRD - .over(tendermint_max_block_space_in_bytes) - as usize, - threshold::ONE_THIRD + threshold::ONE_HALF .over(tendermint_max_block_space_in_bytes) as usize, - threshold::ONE_THIRD + threshold::ONE_HALF .over(tendermint_max_block_space_in_bytes) as usize, ) diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs index 7163cdf877..94ba14f574 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs @@ -6,71 +6,50 @@ //! //! The state machine moves through the following state DAG: //! -//! 1. [`BuildingEncryptedTxBatch`] - the initial state. In this state, we -//! populate a block with DKG encrypted txs. This state supports two modes of -//! operation, which you can think of as two sub-states: -//! * [`WithoutEncryptedTxs`] - When this mode is active, no encrypted txs are -//! included in a block proposal. -//! * [`WithEncryptedTxs`] - When this mode is active, we are able to include -//! encrypted txs in a block proposal. -//! 2. [`BuildingDecryptedTxBatch`] - the second state. In this state, we -//! populate a block with DKG decrypted txs. -//! 3. [`BuildingProtocolTxBatch`] - the third state. In this state, we populate -//! a block with protocol txs. +//! 1. [`BuildingProtocolTxBatch`] - the initial state. In +//! this state, we populate a block with protocol txs. +//! 2. [`BuildingTxBatch`] - the second state. In +//! this state, we populate a block with non-protocol txs. +//! 3. [`BuildingProtocolTxBatch`] - we return to this state to +//! fill up any remaining block space if possible. -mod decrypted_txs; -mod encrypted_txs; +mod normal_txs; mod protocol_txs; -use super::{AllocFailure, BlockAllocator}; - -/// Convenience wrapper for a [`BlockAllocator`] state that allocates -/// encrypted transactions. -#[allow(dead_code)] -pub enum EncryptedTxBatchAllocator { - WithEncryptedTxs( - BlockAllocator>, - ), - WithoutEncryptedTxs( - BlockAllocator>, - ), -} +use super::AllocFailure; /// The leader of the current Tendermint round is building -/// a new batch of DKG decrypted transactions. +/// a new batch of protocol txs. /// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::block_alloc::states`]. -pub enum BuildingDecryptedTxBatch {} - -/// The leader of the current Tendermint round is building -/// a new batch of Namada protocol transactions. +/// This happens twice, in the first stage, we fill up to 1/2 +/// of the block. At the end of allocating user txs, we fill +/// up any remaining space with un-allocated protocol txs. /// /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. -pub enum BuildingProtocolTxBatch {} +pub struct BuildingProtocolTxBatch { + /// One of [`WithEncryptedTxs`] and [`WithoutEncryptedTxs`]. + _mode: Mode, +} -/// The leader of the current Tendermint round is building -/// a new batch of DKG encrypted transactions. +/// Allow block proposals to include user submitted txs. /// /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. -pub struct BuildingEncryptedTxBatch { - /// One of [`WithEncryptedTxs`] and [`WithoutEncryptedTxs`]. - _mode: Mode, -} +pub enum WithNormalTxs {} /// Allow block proposals to include encrypted txs. /// /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. -pub enum WithEncryptedTxs {} +pub enum WithoutNormalTxs {} -/// Prohibit block proposals from including encrypted txs. +/// The leader of the current Tendermint round is building +/// a new batch of user submitted (non-protocol) transactions. /// /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. -pub enum WithoutEncryptedTxs {} +pub struct BuildingTxBatch {} /// Try to allocate a new transaction on a [`BlockAllocator`] state. /// diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/encrypted_txs.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/encrypted_txs.rs deleted file mode 100644 index 05f74d1d56..0000000000 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/encrypted_txs.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::marker::PhantomData; - -use super::super::{AllocFailure, BlockAllocator, TxBin}; -use super::{ - BuildingDecryptedTxBatch, BuildingEncryptedTxBatch, - EncryptedTxBatchAllocator, NextStateImpl, TryAlloc, WithEncryptedTxs, - WithoutEncryptedTxs, -}; -use crate::node::ledger::shell::block_alloc::BlockResources; - -impl TryAlloc for BlockAllocator> { - type Resources<'tx> = BlockResources<'tx>; - - #[inline] - fn try_alloc( - &mut self, - resource_required: Self::Resources<'_>, - ) -> Result<(), AllocFailure> { - self.encrypted_txs.space.try_dump(resource_required.tx)?; - self.encrypted_txs.gas.try_dump(resource_required.gas) - } -} - -impl NextStateImpl - for BlockAllocator> -{ - type Next = BlockAllocator; - - #[inline] - fn next_state_impl(self) -> Self::Next { - next_state(self) - } -} - -impl TryAlloc - for BlockAllocator> -{ - type Resources<'tx> = BlockResources<'tx>; - - #[inline] - fn try_alloc( - &mut self, - _resource_required: Self::Resources<'_>, - ) -> Result<(), AllocFailure> { - Err(AllocFailure::Rejected { - bin_resource_left: 0, - }) - } -} - -impl NextStateImpl - for BlockAllocator> -{ - type Next = BlockAllocator; - - #[inline] - fn next_state_impl(self) -> Self::Next { - next_state(self) - } -} - -#[inline] -fn next_state( - mut alloc: BlockAllocator>, -) -> BlockAllocator { - alloc.encrypted_txs.space.shrink_to_fit(); - - // decrypted txs can use as much space as they need - which - // in practice will only be, at most, 1/3 of the block space - // used by encrypted txs at the prev height - let remaining_free_space = alloc.uninitialized_space_in_bytes(); - alloc.decrypted_txs = TxBin::init(remaining_free_space); - - // cast state - let BlockAllocator { - block, - protocol_txs, - encrypted_txs, - decrypted_txs, - .. - } = alloc; - - BlockAllocator { - _state: PhantomData, - block, - protocol_txs, - encrypted_txs, - decrypted_txs, - } -} - -impl TryAlloc for EncryptedTxBatchAllocator { - type Resources<'tx> = BlockResources<'tx>; - - #[inline] - fn try_alloc( - &mut self, - resource_required: Self::Resources<'_>, - ) -> Result<(), AllocFailure> { - match self { - EncryptedTxBatchAllocator::WithEncryptedTxs(state) => { - state.try_alloc(resource_required) - } - EncryptedTxBatchAllocator::WithoutEncryptedTxs(state) => { - // NOTE: this operation will cause the allocator to - // run out of memory immediately - state.try_alloc(resource_required) - } - } - } -} - -impl NextStateImpl for EncryptedTxBatchAllocator { - type Next = BlockAllocator; - - #[inline] - fn next_state_impl(self) -> Self::Next { - match self { - EncryptedTxBatchAllocator::WithEncryptedTxs(state) => { - state.next_state_impl() - } - EncryptedTxBatchAllocator::WithoutEncryptedTxs(state) => { - state.next_state_impl() - } - } - } -} diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/decrypted_txs.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs similarity index 51% rename from crates/apps/src/lib/node/ledger/shell/block_alloc/states/decrypted_txs.rs rename to crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs index 7d7cc51d90..b024333367 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/decrypted_txs.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs @@ -2,38 +2,36 @@ use std::marker::PhantomData; use super::super::{AllocFailure, BlockAllocator, TxBin}; use super::{ - BuildingDecryptedTxBatch, BuildingProtocolTxBatch, NextStateImpl, TryAlloc, + BuildingProtocolTxBatch, BuildingTxBatch, NextStateImpl, TryAlloc, + WithoutNormalTxs, }; +use crate::node::ledger::shell::block_alloc::BlockResources; -impl TryAlloc for BlockAllocator { - type Resources<'tx> = &'tx [u8]; +impl TryAlloc for BlockAllocator { + type Resources<'tx> = BlockResources<'tx>; #[inline] fn try_alloc( &mut self, - tx: Self::Resources<'_>, + resource_required: Self::Resources<'_>, ) -> Result<(), AllocFailure> { - self.decrypted_txs.try_dump(tx) + self.normal_txs.space.try_dump(resource_required.tx)?; + self.normal_txs.gas.try_dump(resource_required.gas) } } -impl NextStateImpl for BlockAllocator { - type Next = BlockAllocator; +impl NextStateImpl for BlockAllocator { + type Next = BlockAllocator>; #[inline] fn next_state_impl(mut self) -> Self::Next { - self.decrypted_txs.shrink_to_fit(); - - // the remaining space is allocated to protocol txs let remaining_free_space = self.uninitialized_space_in_bytes(); self.protocol_txs = TxBin::init(remaining_free_space); - // cast state let Self { block, protocol_txs, - encrypted_txs, - decrypted_txs, + normal_txs, .. } = self; @@ -41,8 +39,7 @@ impl NextStateImpl for BlockAllocator { _state: PhantomData, block, protocol_txs, - encrypted_txs, - decrypted_txs, + normal_txs, } } } diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs index aba289113e..97b642daff 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs @@ -1,7 +1,13 @@ +use std::marker::PhantomData; + use super::super::{AllocFailure, BlockAllocator}; -use super::{BuildingProtocolTxBatch, TryAlloc}; +use super::{ + BuildingProtocolTxBatch, BuildingTxBatch, NextStateImpl, TryAlloc, + WithNormalTxs, +}; +use crate::node::ledger::shell::block_alloc::TxBin; -impl TryAlloc for BlockAllocator { +impl TryAlloc for BlockAllocator> { type Resources<'tx> = &'tx [u8]; #[inline] @@ -12,3 +18,28 @@ impl TryAlloc for BlockAllocator { self.protocol_txs.try_dump(tx) } } + +impl NextStateImpl for BlockAllocator> { + type Next = BlockAllocator; + + #[inline] + fn next_state_impl(mut self) -> Self::Next { + self.protocol_txs.shrink_to_fit(); + let remaining_free_space = self.uninitialized_space_in_bytes(); + self.normal_txs.space = TxBin::init(remaining_free_space); + // cast state + let BlockAllocator { + block, + protocol_txs, + normal_txs, + .. + } = self; + + BlockAllocator { + _state: PhantomData, + block, + protocol_txs, + normal_txs, + } + } +} diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index ec0e535c4d..c4c4de88de 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -201,84 +201,38 @@ where format!("Tx rejected: {}", &processed_tx.result.info); tx_event["gas_used"] = "0".into(); response.events.push(tx_event); - // if the rejected tx was decrypted, remove it - // from the queue of txs to be processed - if let TxType::Decrypted(_) = &tx_header.tx_type { - self.state - .in_mem_mut() - .tx_queue - .pop() - .expect("Missing wrapper tx in queue"); - } - continue; } let ( mut tx_event, embedding_wrapper, - tx_gas_meter, - wrapper, + mut tx_gas_meter, mut wrapper_args, ) = match &tx_header.tx_type { TxType::Wrapper(wrapper) => { stats.increment_wrapper_txs(); let tx_event = new_tx_event(&tx, height.0); let gas_meter = TxGasMeter::new(wrapper.gas_limit); + if let Some(code_sec) = tx + .get_section(tx.code_sechash()) + .and_then(|x| Section::code_sec(x.as_ref())) + { + stats.increment_tx_type( + code_sec.code.hash().to_string(), + ); + } ( tx_event, None, gas_meter, - Some(tx.clone()), Some(WrapperArgs { block_proposer: &native_block_proposer_address, is_committed_fee_unshield: false, }), ) } - TxType::Decrypted(inner) => { - // We remove the corresponding wrapper tx from the queue - let tx_in_queue = self - .state - .in_mem_mut() - .tx_queue - .pop() - .expect("Missing wrapper tx in queue"); - let mut event = new_tx_event(&tx, height.0); - - match inner { - DecryptedTx::Decrypted => { - if let Some(code_sec) = tx - .get_section(tx.code_sechash()) - .and_then(|x| Section::code_sec(x.as_ref())) - { - stats.increment_tx_type( - code_sec.code.hash().to_string(), - ); - } - } - DecryptedTx::Undecryptable => { - tracing::info!( - "Tx with hash {} was un-decryptable", - tx_in_queue.tx.header_hash() - ); - event["info"] = "Transaction is invalid.".into(); - event["log"] = - "Transaction could not be decrypted.".into(); - event["code"] = ResultCode::Undecryptable.into(); - response.events.push(event); - continue; - } - } - - ( - event, - Some(tx_in_queue.tx), - TxGasMeter::new_from_sub_limit(tx_in_queue.gas), - None, - None, - ) - } + TxType::Decrypted(_) => unreachable!(), TxType::Raw => { tracing::error!( "Internal logic error: FinalizeBlock received a \ @@ -295,7 +249,6 @@ where None, TxGasMeter::new_from_sub_limit(0.into()), None, - None, ), ProtocolTxType::EthEventsVext => { let ext = @@ -320,7 +273,6 @@ where None, TxGasMeter::new_from_sub_limit(0.into()), None, - None, ) } ProtocolTxType::EthereumEvents => { @@ -348,7 +300,6 @@ where None, TxGasMeter::new_from_sub_limit(0.into()), None, - None, ) } }, @@ -376,44 +327,33 @@ where match tx_result { Ok(result) => { if result.is_accepted() { - if let EventType::Accepted = tx_event.event_type { - // Wrapper transaction - tracing::trace!( - "Wrapper transaction {} was accepted", - tx_event["hash"] - ); - if wrapper_args - .expect("Missing required wrapper arguments") - .is_committed_fee_unshield - { - tx_event["is_valid_masp_tx"] = - format!("{}", tx_index); - } - self.state.in_mem_mut().tx_queue.push(TxInQueue { - tx: wrapper.expect("Missing expected wrapper"), - gas: tx_gas_meter.get_available_gas(), - }); - } else { - tracing::trace!( - "all VPs accepted transaction {} storage \ - modification {:#?}", - tx_event["hash"], - result - ); - if result.vps_result.accepted_vps.contains( + if wrapper_args + .expect("Missing required wrapper arguments") + .is_committed_fee_unshield + || result.vps_result.accepted_vps.contains( &Address::Internal( address::InternalAddress::Masp, ), - ) { - tx_event["is_valid_masp_tx"] = - format!("{}", tx_index); - } - changed_keys - .extend(result.changed_keys.iter().cloned()); - stats.increment_successful_txs(); - if let Some(wrapper) = embedding_wrapper { - self.commit_inner_tx_hash(wrapper); - } + ) + { + tx_event["is_valid_masp_tx"] = + format!("{}", tx_index); + } + tracing::trace!( + "all VPs accepted transaction {} storage \ + modification {:#?}", + tx_event["hash"], + result + ); + + changed_keys + .extend(result.changed_keys.iter().cloned()); + changed_keys.extend( + result.wrapper_changed_keys.iter().cloned(), + ); + stats.increment_successful_txs(); + if let Some(wrapper) = embedding_wrapper { + self.commit_inner_tx_hash(wrapper); } self.state.commit_tx(); if !tx_event.contains_key("code") { @@ -514,21 +454,18 @@ where tx_event["gas_used"] = tx_gas_meter.get_tx_consumed_gas().to_string(); tx_event["info"] = msg.to_string(); - if let EventType::Accepted = tx_event.event_type { - // If wrapper, invalid tx error code - tx_event["code"] = ResultCode::InvalidTx.into(); - // The fee unshield operation could still have been - // committed - if wrapper_args - .expect("Missing required wrapper arguments") - .is_committed_fee_unshield - { - tx_event["is_valid_masp_tx"] = - format!("{}", tx_index); - } - } else { - tx_event["code"] = ResultCode::WasmRuntimeError.into(); + + // If wrapper, invalid tx error code + tx_event["code"] = ResultCode::InvalidTx.into(); + // The fee unshield operation could still have been + // committed + if wrapper_args + .expect("Missing required wrapper arguments") + .is_committed_fee_unshield + { + tx_event["is_valid_masp_tx"] = format!("{}", tx_index); } + tx_event["code"] = ResultCode::WasmRuntimeError.into(); } } response.events.push(tx_event); @@ -842,44 +779,6 @@ mod test_finalize_block { ) } - /// Make a wrapper tx and a processed tx from the wrapped tx that can be - /// added to `FinalizeBlock` request. - fn mk_decrypted_tx( - shell: &mut TestShell, - keypair: &common::SecretKey, - ) -> ProcessedTx { - let tx_code = TestWasms::TxNoOp.read_bytes(); - let mut outer_tx = - Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native(1.into()), - token: shell.state.in_mem().native_token.clone(), - }, - keypair.ref_to(), - Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), - None, - )))); - outer_tx.header.chain_id = shell.chain_id.clone(); - outer_tx.set_code(Code::new(tx_code, None)); - outer_tx.set_data(Data::new( - "Decrypted transaction data".as_bytes().to_owned(), - )); - let gas_limit = - Gas::from(outer_tx.header().wrapper().unwrap().gas_limit) - .checked_sub(Gas::from(outer_tx.to_bytes().len() as u64)) - .unwrap(); - shell.enqueue_tx(outer_tx.clone(), gas_limit); - outer_tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); - ProcessedTx { - tx: outer_tx.to_bytes().into(), - result: TxResult { - code: ResultCode::Ok.into(), - info: "".into(), - }, - } - } - /// Check that if a wrapper tx was rejected by [`process_proposal`], /// check that the correct event is returned. Check that it does /// not appear in the queue of txs to be decrypted @@ -890,7 +789,7 @@ mod test_finalize_block { let mut processed_txs = vec![]; let mut valid_wrappers = vec![]; - // Add unshielded balance for fee paymenty + // Add unshielded balance for fee payment let balance_key = token::storage_key::balance_key( &shell.state.in_mem().native_token, &Address::from(&keypair.ref_to()), @@ -903,28 +802,11 @@ mod test_finalize_block { // create some wrapper txs for i in 1u64..5 { let (wrapper, mut processed_tx) = mk_wrapper_tx(&shell, &keypair); - if i > 1 { - processed_tx.result.code = - u32::try_from(i.rem_euclid(2)).unwrap(); - processed_txs.push(processed_tx); - } else { - let wrapper_info = - if let TxType::Wrapper(w) = wrapper.header().tx_type { - w - } else { - panic!("Unexpected tx type"); - }; - shell.enqueue_tx( - wrapper.clone(), - Gas::from(wrapper_info.gas_limit) - .checked_sub(Gas::from(wrapper.to_bytes().len() as u64)) - .unwrap(), - ); - } - - if i != 3 { - valid_wrappers.push(wrapper) + processed_tx.result.code = u32::try_from(i.rem_euclid(2)).unwrap(); + if processed_tx.result.code != 0 { + valid_wrappers.push(wrapper); } + processed_txs.push(processed_tx); } // check that the correct events were created @@ -941,204 +823,6 @@ mod test_finalize_block { let code = event.attributes.get("code").expect("Test failed"); assert_eq!(code, &index.rem_euclid(2).to_string()); } - // verify that the queue of wrapper txs to be processed is correct - let mut valid_tx = valid_wrappers.iter(); - let mut counter = 0; - for wrapper in shell.iter_tx_queue() { - // we cannot easily implement the PartialEq trait for WrapperTx - // so we check the hashes of the inner txs for equality - let valid_tx = valid_tx.next().expect("Test failed"); - assert_eq!(wrapper.tx.header.code_hash, *valid_tx.code_sechash()); - assert_eq!(wrapper.tx.header.data_hash, *valid_tx.data_sechash()); - counter += 1; - } - assert_eq!(counter, 3); - } - - /// Check that if a decrypted tx was rejected by [`process_proposal`], - /// the correct event is returned. Check that it is still - /// removed from the queue of txs to be included in the next block - /// proposal - #[test] - fn test_process_proposal_rejected_decrypted_tx() { - let (mut shell, _, _, _) = setup(); - let keypair = gen_keypair(); - let mut outer_tx = - Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native( - Default::default(), - ), - token: shell.state.in_mem().native_token.clone(), - }, - keypair.ref_to(), - Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), - None, - )))); - outer_tx.header.chain_id = shell.chain_id.clone(); - outer_tx.set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); - outer_tx.set_data(Data::new( - String::from("transaction data").as_bytes().to_owned(), - )); - let gas_limit = - Gas::from(outer_tx.header().wrapper().unwrap().gas_limit) - .checked_sub(Gas::from(outer_tx.to_bytes().len() as u64)) - .unwrap(); - shell.enqueue_tx(outer_tx.clone(), gas_limit); - - outer_tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); - let processed_tx = ProcessedTx { - tx: outer_tx.to_bytes().into(), - result: TxResult { - code: ResultCode::InvalidTx.into(), - info: "".into(), - }, - }; - - // check that the decrypted tx was not applied - for event in shell - .finalize_block(FinalizeBlock { - txs: vec![processed_tx], - ..Default::default() - }) - .expect("Test failed") - { - assert_eq!(event.event_type.to_string(), String::from("applied")); - let code = event.attributes.get("code").expect("Test failed"); - assert_eq!(code, &String::from(ResultCode::InvalidTx)); - } - // check that the corresponding wrapper tx was removed from the queue - assert!(shell.state.in_mem().tx_queue.is_empty()); - } - - /// Test that if a tx is undecryptable, it is applied - /// but the tx result contains the appropriate error code. - #[test] - fn test_undecryptable_returns_error_code() { - let (mut shell, _, _, _) = setup(); - - let keypair = crate::wallet::defaults::daewon_keypair(); - // not valid tx bytes - let wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native(0.into()), - token: shell.state.in_mem().native_token.clone(), - }, - keypair.ref_to(), - Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), - None, - )))); - let processed_tx = ProcessedTx { - tx: Tx::from_type(TxType::Decrypted(DecryptedTx::Undecryptable)) - .to_bytes() - .into(), - result: TxResult { - code: ResultCode::Ok.into(), - info: "".into(), - }, - }; - - let gas_limit = - Gas::from(wrapper.header().wrapper().unwrap().gas_limit) - .checked_sub(Gas::from(wrapper.to_bytes().len() as u64)) - .unwrap(); - shell.enqueue_tx(wrapper, gas_limit); - - // check that correct error message is returned - for event in shell - .finalize_block(FinalizeBlock { - txs: vec![processed_tx], - ..Default::default() - }) - .expect("Test failed") - { - assert_eq!(event.event_type.to_string(), String::from("applied")); - let code = event.attributes.get("code").expect("Test failed"); - assert_eq!(code, &String::from(ResultCode::Undecryptable)); - let log = event.attributes.get("log").expect("Test failed"); - assert!(log.contains("Transaction could not be decrypted.")) - } - // check that the corresponding wrapper tx was removed from the queue - assert!(shell.state.in_mem().tx_queue.is_empty()); - } - - /// Test that the wrapper txs are queued in the order they - /// are received from the block. Tests that the previously - /// decrypted txs are de-queued. - #[test] - fn test_mixed_txs_queued_in_correct_order() { - let (mut shell, _, _, _) = setup(); - let keypair = gen_keypair(); - let mut processed_txs = vec![]; - let mut valid_txs = vec![]; - - // Add unshielded balance for fee payment - let balance_key = token::storage_key::balance_key( - &shell.state.in_mem().native_token, - &Address::from(&keypair.ref_to()), - ); - shell - .state - .write(&balance_key, Amount::native_whole(1000)) - .unwrap(); - - // create two decrypted txs - for _ in 0..2 { - processed_txs.push(mk_decrypted_tx(&mut shell, &keypair)); - } - // create two wrapper txs - for _ in 0..2 { - let (tx, processed_tx) = mk_wrapper_tx(&shell, &keypair); - valid_txs.push(tx.clone()); - processed_txs.push(processed_tx); - } - // Put the wrapper txs in front of the decrypted txs - processed_txs.rotate_left(2); - // check that the correct events were created - for (index, event) in shell - .finalize_block(FinalizeBlock { - txs: processed_txs, - ..Default::default() - }) - .expect("Test failed") - .iter() - .enumerate() - { - if index < 2 { - // these should be accepted wrapper txs - assert_eq!( - event.event_type.to_string(), - String::from("accepted") - ); - let code = - event.attributes.get("code").expect("Test failed").as_str(); - assert_eq!(code, String::from(ResultCode::Ok).as_str()); - } else { - // these should be accepted decrypted txs - assert_eq!( - event.event_type.to_string(), - String::from("applied") - ); - let code = - event.attributes.get("code").expect("Test failed").as_str(); - assert_eq!(code, String::from(ResultCode::Ok).as_str()); - } - } - - // check that the applied decrypted txs were dequeued and the - // accepted wrappers were enqueued in correct order - let mut txs = valid_txs.iter(); - - let mut counter = 0; - for wrapper in shell.iter_tx_queue() { - let next = txs.next().expect("Test failed"); - assert_eq!(wrapper.tx.header.code_hash, *next.code_sechash()); - assert_eq!(wrapper.tx.header.data_hash, *next.data_sechash()); - counter += 1; - } - assert_eq!(counter, 2); } /// Test if a rejected protocol tx is applied and emits @@ -1604,10 +1288,6 @@ mod test_finalize_block { for _ in 0..20 { // Add some txs let mut txs = vec![]; - // create two decrypted txs - for _ in 0..2 { - txs.push(mk_decrypted_tx(&mut shell, &txs_key)); - } // create two wrapper txs for _ in 0..2 { let (_tx, processed_tx) = mk_wrapper_tx(&shell, &txs_key); @@ -2740,7 +2420,7 @@ mod test_finalize_block { ); } - /// Test that a decrypted tx that has already been applied in the same block + /// Test that a tx that has already been applied in the same block /// doesn't get reapplied #[test] fn test_duplicated_decrypted_tx_same_block() { @@ -2763,9 +2443,7 @@ mod test_finalize_block { )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new(tx_code, None)); - wrapper.set_data(Data::new( - "Decrypted transaction data".as_bytes().to_owned(), - )); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); let mut new_wrapper = wrapper.clone(); new_wrapper.update_header(TxType::Wrapper(Box::new(WrapperTx::new( @@ -2789,12 +2467,8 @@ mod test_finalize_block { None, ))); - let mut inner = wrapper.clone(); - let mut new_inner = new_wrapper.clone(); - - for inner in [&mut inner, &mut new_inner] { - inner.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); - } + let inner = wrapper.clone(); + let new_inner = new_wrapper.clone(); // Write wrapper hashes in storage for tx in [&wrapper, &new_wrapper] { @@ -2816,8 +2490,6 @@ mod test_finalize_block { }) } - shell.enqueue_tx(wrapper.clone(), GAS_LIMIT_MULTIPLIER.into()); - shell.enqueue_tx(new_wrapper.clone(), GAS_LIMIT_MULTIPLIER.into()); // merkle tree root before finalize_block let root_pre = shell.shell.state.in_mem().block.tree.root(); @@ -2857,8 +2529,8 @@ mod test_finalize_block { } } - /// Test that if a decrypted transaction fails because of out-of-gas, - /// undecryptable, invalid signature or wrong section commitment, its hash + /// Test that if a transaction fails because of out-of-gas, + /// invalid signature or wrong section commitment, its hash /// is not committed to storage. Also checks that a tx failing for other /// reason has its hash written to storage. #[test] @@ -2868,7 +2540,6 @@ mod test_finalize_block { let mut batch = namada::state::testing::TestState::batch(); let (out_of_gas_wrapper, _) = mk_wrapper_tx(&shell, &keypair); - let (undecryptable_wrapper, _) = mk_wrapper_tx(&shell, &keypair); let mut wasm_path = top_level_directory(); // Write a key to trigger the vp to validate the signature wasm_path.push("wasm_for_tests/tx_write.wasm"); @@ -2911,30 +2582,17 @@ mod test_finalize_block { let mut wrong_commitment_wrapper = failing_wrapper.clone(); wrong_commitment_wrapper.set_code_sechash(Hash::default()); - let mut out_of_gas_inner = out_of_gas_wrapper.clone(); - let mut undecryptable_inner = undecryptable_wrapper.clone(); - let mut unsigned_inner = unsigned_wrapper.clone(); + let out_of_gas_inner = out_of_gas_wrapper.clone(); + let unsigned_inner = unsigned_wrapper.clone(); let mut wrong_commitment_inner = failing_wrapper.clone(); // Add some extra data to avoid having the same Tx hash as the // `failing_wrapper` wrong_commitment_inner.add_memo(&[0_u8]); - let mut failing_inner = failing_wrapper.clone(); - - undecryptable_inner - .update_header(TxType::Decrypted(DecryptedTx::Undecryptable)); - for inner in [ - &mut out_of_gas_inner, - &mut unsigned_inner, - &mut wrong_commitment_inner, - &mut failing_inner, - ] { - inner.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); - } + let failing_inner = failing_wrapper.clone(); // Write wrapper hashes in storage for wrapper in [ &out_of_gas_wrapper, - &undecryptable_wrapper, &unsigned_wrapper, &wrong_commitment_wrapper, &failing_wrapper, @@ -2950,7 +2608,6 @@ mod test_finalize_block { let mut processed_txs: Vec = vec![]; for inner in [ &out_of_gas_inner, - &undecryptable_inner, &unsigned_inner, &wrong_commitment_inner, &failing_inner, @@ -2964,17 +2621,6 @@ mod test_finalize_block { }) } - shell.enqueue_tx(out_of_gas_wrapper.clone(), Gas::default()); - shell.enqueue_tx( - undecryptable_wrapper.clone(), - GAS_LIMIT_MULTIPLIER.into(), - ); - shell.enqueue_tx(unsigned_wrapper.clone(), u64::MAX.into()); // Prevent out of gas which would still make the test pass - shell.enqueue_tx( - wrong_commitment_wrapper.clone(), - GAS_LIMIT_MULTIPLIER.into(), - ); - shell.enqueue_tx(failing_wrapper.clone(), GAS_LIMIT_MULTIPLIER.into()); // merkle tree root before finalize_block let root_pre = shell.shell.state.in_mem().block.tree.root(); @@ -3007,7 +2653,6 @@ mod test_finalize_block { for (invalid_inner, valid_wrapper) in [ (out_of_gas_inner, out_of_gas_wrapper), - (undecryptable_inner, undecryptable_wrapper), (unsigned_inner, unsigned_wrapper), (wrong_commitment_inner, wrong_commitment_wrapper), ] { @@ -3226,9 +2871,7 @@ mod test_finalize_block { )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new(tx_code, None)); - wrapper.set_data(Data::new( - "Enxrypted transaction data".as_bytes().to_owned(), - )); + wrapper.set_data(Data::new("Transaction data".as_bytes().to_owned())); wrapper.add_section(Section::Signature(Signature::new( wrapper.sechashes(), [(0, crate::wallet::defaults::albert_keypair())] diff --git a/crates/apps/src/lib/node/ledger/shell/governance.rs b/crates/apps/src/lib/node/ledger/shell/governance.rs index e568f5e212..5a3a619295 100644 --- a/crates/apps/src/lib/node/ledger/shell/governance.rs +++ b/crates/apps/src/lib/node/ledger/shell/governance.rs @@ -299,7 +299,7 @@ where let pending_execution_key = gov_storage::get_proposal_execution_key(id); shell.state.write(&pending_execution_key, ())?; - let mut tx = Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted)); + let mut tx = Tx::from_type(TxType::Raw); tx.header.chain_id = shell.chain_id.clone(); tx.set_data(Data::new(encode(&id))); tx.set_code(Code::new(code, None)); diff --git a/crates/apps/src/lib/node/ledger/shell/mod.rs b/crates/apps/src/lib/node/ledger/shell/mod.rs index e144794e2c..672f3842ce 100644 --- a/crates/apps/src/lib/node/ledger/shell/mod.rs +++ b/crates/apps/src/lib/node/ledger/shell/mod.rs @@ -55,14 +55,14 @@ use namada::ledger::protocol::{ use namada::ledger::{parameters, protocol}; use namada::parameters::validate_tx_bytes; use namada::proof_of_stake::storage::read_pos_params; -use namada::state::tx_queue::{ExpiredTx, TxInQueue}; +use namada::state::tx_queue::ExpiredTx; use namada::state::{ DBIter, FullAccessState, Sha256Hasher, StorageHasher, StorageRead, TempWlState, WlState, DB, EPOCH_SWITCH_BLOCKS_DELAY, }; use namada::token; pub use namada::tx::data::ResultCode; -use namada::tx::data::{DecryptedTx, TxType, WrapperTx, WrapperTxErr}; +use namada::tx::data::{TxType, WrapperTx, WrapperTxErr}; use namada::tx::{Section, Tx}; use namada::vm::wasm::{TxCache, VpCache}; use namada::vm::{WasmCacheAccess, WasmCacheRwAccess}; @@ -542,12 +542,6 @@ where &mut self.event_log } - /// Iterate over the wrapper txs in order - #[allow(dead_code)] - fn iter_tx_queue(&mut self) -> impl Iterator { - self.state.in_mem().tx_queue.iter() - } - /// Load the Merkle root hash and the height of the last committed block, if /// any. This is returned when ABCI sends an `info` request. pub fn last_state(&mut self) -> response::Info { @@ -1427,12 +1421,10 @@ mod test_utils { use crate::facade::tendermint_proto::v0_37::abci::{ RequestPrepareProposal, RequestProcessProposal, }; - use crate::node::ledger::shell::token::DenominatedAmount; use crate::node::ledger::shims::abcipp_shim_types; use crate::node::ledger::shims::abcipp_shim_types::shim::request::{ FinalizeBlock, ProcessedTx, }; - use crate::node::ledger::storage::{PersistentDB, PersistentStorageHasher}; #[derive(Error, Debug)] pub enum TestError { @@ -1700,17 +1692,6 @@ mod test_utils { self.shell.prepare_proposal(req) } - /// Add a wrapper tx to the queue of txs to be decrypted - /// in the current block proposal. Takes the length of the encoded - /// wrapper as parameter. - #[cfg(test)] - pub fn enqueue_tx(&mut self, tx: Tx, inner_tx_gas: Gas) { - self.shell.state.in_mem_mut().tx_queue.push(TxInQueue { - tx, - gas: inner_tx_gas, - }); - } - /// Start a counter for the next epoch in `num_blocks`. pub fn start_new_epoch_in(&mut self, num_blocks: u64) { self.state.in_mem_mut().next_epoch_min_start_height = @@ -1898,130 +1879,6 @@ mod test_utils { .expect("Test failed"); } - /// We test that on shell shutdown, the tx queue gets persisted in a DB, and - /// on startup it is read successfully - #[test] - fn test_tx_queue_persistence() { - let base_dir = tempdir().unwrap().as_ref().canonicalize().unwrap(); - // we have to use RocksDB for this test - let (sender, _) = tokio::sync::mpsc::unbounded_channel(); - let (_, eth_receiver) = - tokio::sync::mpsc::channel(ORACLE_CHANNEL_BUFFER_SIZE); - let (control_sender, _) = oracle::control::channel(); - let (_, last_processed_block_receiver) = - last_processed_block::channel(); - let eth_oracle = EthereumOracleChannels::new( - eth_receiver, - control_sender, - last_processed_block_receiver, - ); - let vp_wasm_compilation_cache = 50 * 1024 * 1024; // 50 kiB - let tx_wasm_compilation_cache = 50 * 1024 * 1024; // 50 kiB - let native_token = address::testing::nam(); - let mut shell = Shell::::new( - config::Ledger::new( - base_dir.clone(), - Default::default(), - TendermintMode::Validator, - ), - top_level_directory().join("wasm"), - sender.clone(), - Some(eth_oracle), - None, - vp_wasm_compilation_cache, - tx_wasm_compilation_cache, - ); - shell - .state - .in_mem_mut() - .begin_block(BlockHash::default(), BlockHeight(1)) - .expect("begin_block failed"); - let keypair = gen_keypair(); - // enqueue a wrapper tx - let mut wrapper = - Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native(0.into()), - token: native_token, - }, - keypair.ref_to(), - Epoch(0), - 300_000.into(), - None, - )))); - wrapper.header.chain_id = shell.chain_id.clone(); - wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); - wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); - - shell.state.in_mem_mut().tx_queue.push(TxInQueue { - tx: wrapper, - gas: u64::MAX.into(), - }); - // Artificially increase the block height so that chain - // will read the new block when restarted - shell - .state - .in_mem_mut() - .block - .pred_epochs - .new_epoch(BlockHeight(1)); - // initialize parameter storage - let params = Parameters { - max_tx_bytes: 1024 * 1024, - epoch_duration: EpochDuration { - min_num_of_blocks: 1, - min_duration: DurationSecs(3600), - }, - max_expected_time_per_block: DurationSecs(3600), - max_proposal_bytes: Default::default(), - max_block_gas: 100, - vp_allowlist: vec![], - tx_allowlist: vec![], - implicit_vp_code_hash: Default::default(), - epochs_per_year: 365, - max_signatures_per_transaction: 10, - staked_ratio: Default::default(), - pos_inflation_amount: Default::default(), - fee_unshielding_gas_limit: 0, - fee_unshielding_descriptions_limit: 0, - minimum_gas_price: Default::default(), - }; - parameters::init_storage(¶ms, &mut shell.state) - .expect("Test failed"); - // make state to update conversion for a new epoch - update_allowed_conversions(&mut shell.state) - .expect("update conversions failed"); - shell.state.commit_block().expect("commit failed"); - - // Drop the shell - std::mem::drop(shell); - let (_, eth_receiver) = - tokio::sync::mpsc::channel(ORACLE_CHANNEL_BUFFER_SIZE); - let (control_sender, _) = oracle::control::channel(); - let (_, last_processed_block_receiver) = - last_processed_block::channel(); - let eth_oracle = EthereumOracleChannels::new( - eth_receiver, - control_sender, - last_processed_block_receiver, - ); - // Reboot the shell and check that the queue was restored from DB - let shell = Shell::::new( - config::Ledger::new( - base_dir, - Default::default(), - TendermintMode::Validator, - ), - top_level_directory().join("wasm"), - sender, - Some(eth_oracle), - None, - vp_wasm_compilation_cache, - tx_wasm_compilation_cache, - ); - assert!(!shell.state.in_mem().tx_queue.is_empty()); - } - pub(super) fn get_pkh_from_address( storage: &S, params: &PosParams, diff --git a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs index b08c2bec59..956d0d9404 100644 --- a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -6,7 +6,6 @@ use namada::core::hints; use namada::core::key::tm_raw_hash_to_string; use namada::gas::TxGasMeter; use namada::ledger::protocol; -use namada::ledger::storage::tx_queue::TxInQueue; use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::state::{DBIter, StorageHasher, TempWlState, DB}; use namada::tx::data::{DecryptedTx, TxType, WrapperTx}; @@ -16,8 +15,8 @@ use namada::vm::WasmCacheAccess; use super::super::*; use super::block_alloc::states::{ - BuildingDecryptedTxBatch, BuildingProtocolTxBatch, - EncryptedTxBatchAllocator, NextState, TryAlloc, + BuildingProtocolTxBatch, BuildingTxBatch, NextState, TryAlloc, + WithNormalTxs, WithoutNormalTxs, }; use super::block_alloc::{AllocFailure, BlockAllocator, BlockResources}; use crate::config::ValidatorLocalConfig; @@ -38,44 +37,46 @@ where /// /// INVARIANT: Any changes applied in this method must be reverted if /// the proposal is rejected (unless we can simply overwrite - /// them in the next block). + /// them in the next block). Furthermore, protocol transactions cannot + /// affect the ability of a tx to pay its wrapper fees. pub fn prepare_proposal( &self, - req: RequestPrepareProposal, + mut req: RequestPrepareProposal, ) -> response::PrepareProposal { let txs = if let ShellMode::Validator { ref local_config, .. } = self.mode { // start counting allotted space for txs - let alloc = self.get_encrypted_txs_allocator(); + let alloc = self.get_protocol_txs_allocator(); + // add vote extension protocol txs + let (alloc, mut txs) = self.build_protocol_txs(alloc, &mut req.txs); + let alloc = alloc.next_state(); // add encrypted txs let tm_raw_hash_string = tm_raw_hash_to_string(req.proposer_address); - let block_proposer = - find_validator_by_raw_hash(&self.state, tm_raw_hash_string) - .unwrap() - .expect( - "Unable to find native validator address of block \ - proposer from tendermint raw hash", - ); - let (encrypted_txs, alloc) = self.build_encrypted_txs( + let block_proposer = find_validator_by_raw_hash( + &self.state, + tm_raw_hash_string, + ) + .unwrap() + .expect( + "Unable to find native validator address of block proposer \ + from tendermint raw hash", + ); + let (mut normal_txs, alloc) = self.build_normal_txs( alloc, &req.txs, req.time, &block_proposer, local_config.as_ref(), ); - let mut txs = encrypted_txs; + txs.append(&mut normal_txs); // decrypt the wrapper txs included in the previous block - let (mut decrypted_txs, alloc) = self.build_decrypted_txs(alloc); - txs.append(&mut decrypted_txs); - - // add vote extension protocol txs - let mut protocol_txs = self.build_protocol_txs(alloc, &req.txs); - txs.append(&mut protocol_txs); - + let (_, mut remaining_txs) = + self.build_protocol_txs(alloc, &mut req.txs); + txs.append(&mut remaining_txs); txs } else { vec![] @@ -90,47 +91,28 @@ where response::PrepareProposal { txs } } - /// Depending on the current block height offset within the epoch, - /// transition state accordingly, return a block space allocator - /// with or without encrypted txs. - /// - /// # How to determine which path to take in the states DAG - /// - /// If we are at the second or third block height offset within an - /// epoch, we do not allow encrypted transactions to be included in - /// a block, therefore we return an allocator wrapped in an - /// [`EncryptedTxBatchAllocator::WithoutEncryptedTxs`] value. - /// Otherwise, we return an allocator wrapped in an - /// [`EncryptedTxBatchAllocator::WithEncryptedTxs`] value. + /// Get the first state of the block allocator. This is for protocol + /// transactions. #[inline] - fn get_encrypted_txs_allocator(&self) -> EncryptedTxBatchAllocator { - let is_2nd_height_off = self.is_deciding_offset_within_epoch(1); - let is_3rd_height_off = self.is_deciding_offset_within_epoch(2); - - if hints::unlikely(is_2nd_height_off || is_3rd_height_off) { - tracing::warn!( - proposal_height = - ?self.state.in_mem().block.height, - "No mempool txs are being included in the current proposal" - ); - EncryptedTxBatchAllocator::WithoutEncryptedTxs( - (&*self.state).into(), - ) - } else { - EncryptedTxBatchAllocator::WithEncryptedTxs((&*self.state).into()) - } + fn get_protocol_txs_allocator( + &self, + ) -> BlockAllocator> { + (&self.state).into() } /// Builds a batch of encrypted transactions, retrieved from /// Tendermint's mempool. - fn build_encrypted_txs( + fn build_normal_txs( &self, - mut alloc: EncryptedTxBatchAllocator, + mut alloc: BlockAllocator, txs: &[TxBytes], block_time: Option, block_proposer: &Address, proposer_local_config: Option<&ValidatorLocalConfig>, - ) -> (Vec, BlockAllocator) { + ) -> ( + Vec, + BlockAllocator>, + ) { let block_time = block_time.and_then(|block_time| { // If error in conversion, default to last block datetime, it's // valid because of mempool check @@ -191,85 +173,24 @@ where (txs, alloc) } - /// Builds a batch of DKG decrypted transactions. - // NOTE: we won't have frontrunning protection until V2 of the - // Anoma protocol; Namada runs V1, therefore this method is - // essentially a NOOP - // - // sources: - // - https://specs.namada.net/main/releases/v2.html - // - https://github.com/anoma/ferveo - fn build_decrypted_txs( - &self, - mut alloc: BlockAllocator, - ) -> (Vec, BlockAllocator) { - let txs = self - .state - .in_mem() - .tx_queue - .iter() - .map( - |TxInQueue { - tx, - gas: _, - }| { - let mut tx = tx.clone(); - tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); - tx.to_bytes().into() - }, - ) - // TODO: make sure all decrypted txs are accepted - .take_while(|tx_bytes: &TxBytes| { - alloc.try_alloc(&tx_bytes[..]).map_or_else( - |status| match status { - AllocFailure::Rejected { bin_resource_left: bin_space_left } => { - tracing::warn!( - ?tx_bytes, - bin_space_left, - proposal_height = - ?self.get_current_decision_height(), - "Dropping decrypted tx from the current proposal", - ); - false - } - AllocFailure::OverflowsBin { bin_resource: bin_size } => { - tracing::warn!( - ?tx_bytes, - bin_size, - proposal_height = - ?self.get_current_decision_height(), - "Dropping large decrypted tx from the current proposal", - ); - true - } - }, - |()| true, - ) - }) - .collect(); - let alloc = alloc.next_state(); - - (txs, alloc) - } - /// Builds a batch of protocol transactions. - fn build_protocol_txs( + fn build_protocol_txs( &self, - mut alloc: BlockAllocator, - txs: &[TxBytes], - ) -> Vec { + mut alloc: BlockAllocator>, + txs: &mut Vec, + ) -> (BlockAllocator>, Vec) { if self.state.in_mem().last_block.is_none() { // genesis should not contain vote extensions. // // this is because we have not decided any block through // consensus yet (hence height 0), which in turn means we // have not committed any vote extensions to a block either. - return vec![]; + return (alloc, vec![]); } - let deserialized_iter = self.deserialize_vote_extensions(txs); + let mut deserialized_iter = self.deserialize_vote_extensions(txs); - deserialized_iter.take_while(|tx_bytes| + let taken = deserialized_iter.by_ref().take_while(|tx_bytes| alloc.try_alloc(&tx_bytes[..]) .map_or_else( |status| match status { @@ -307,7 +228,9 @@ where |()| true, ) ) - .collect() + .collect(); + deserialized_iter.keep_rest(); + (alloc, taken) } } @@ -476,7 +399,7 @@ mod test_prepare_proposal { #[test] fn test_prepare_proposal_rejects_non_wrapper_tx() { let (shell, _recv, _, _) = test_utils::setup(); - let mut tx = Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted)); + let mut tx = Tx::from_type(TxType::Raw); tx.header.chain_id = shell.chain_id.clone(); let req = RequestPrepareProposal { txs: vec![tx.to_bytes().into()], @@ -764,100 +687,6 @@ mod test_prepare_proposal { assert_eq!(signed_eth_ev_vote_extension, rsp_ext.0); } - /// Test that the decrypted txs are included - /// in the proposal in the same order as their - /// corresponding wrappers - #[test] - fn test_decrypted_txs_in_correct_order() { - let (mut shell, _recv, _, _) = test_utils::setup(); - let keypair = gen_keypair(); - let mut expected_wrapper = vec![]; - let mut expected_decrypted = vec![]; - - // Load some tokens to tx signer to pay fees - let balance_key = token::storage_key::balance_key( - &shell.state.in_mem().native_token, - &Address::from(&keypair.ref_to()), - ); - shell - .state - .db_write( - &balance_key, - Amount::native_whole(1_000).serialize_to_vec(), - ) - .unwrap(); - - let mut req = RequestPrepareProposal { - txs: vec![], - ..Default::default() - }; - // create a request with two new wrappers from mempool and - // two wrappers from the previous block to be decrypted - for i in 0..2 { - let mut tx = - Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native( - 1.into(), - ), - token: shell.state.in_mem().native_token.clone(), - }, - keypair.ref_to(), - Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), - None, - )))); - tx.header.chain_id = shell.chain_id.clone(); - tx.set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); - tx.set_data(Data::new( - format!("transaction data: {}", i).as_bytes().to_owned(), - )); - tx.add_section(Section::Signature(Signature::new( - tx.sechashes(), - [(0, keypair.clone())].into_iter().collect(), - None, - ))); - - let gas = Gas::from( - tx.header().wrapper().expect("Wrong tx type").gas_limit, - ) - .checked_sub(Gas::from(tx.to_bytes().len() as u64)) - .unwrap(); - shell.enqueue_tx(tx.clone(), gas); - expected_wrapper.push(tx.clone()); - req.txs.push(tx.to_bytes().into()); - tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); - expected_decrypted.push(tx.clone()); - } - // we extract the inner data from the txs for testing - // equality since otherwise changes in timestamps would - // fail the test - let expected_txs: Vec
= expected_wrapper - .into_iter() - .chain(expected_decrypted) - .map(|tx| tx.header) - .collect(); - let received: Vec
= shell - .prepare_proposal(req) - .txs - .into_iter() - .map(|tx_bytes| { - Tx::try_from(tx_bytes.as_ref()).expect("Test failed").header - }) - .collect(); - // check that the order of the txs is correct - assert_eq!( - received - .iter() - .map(|x| x.serialize_to_vec()) - .collect::>(), - expected_txs - .iter() - .map(|x| x.serialize_to_vec()) - .collect::>(), - ); - } - /// Test that if the unsigned wrapper tx hash is known (replay attack), the /// transaction is not included in the block #[test] diff --git a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs index 73648c7207..be84ef0af2 100644 --- a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -7,7 +7,7 @@ use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::tx::data::protocol::ProtocolTxType; use namada::vote_ext::ethereum_tx_data_variants; -use super::block_alloc::{BlockSpace, EncryptedTxsBins}; +use super::block_alloc::{BlockGas, BlockSpace}; use super::*; use crate::facade::tendermint_proto::v0_37::abci::RequestProcessProposal; use crate::node::ledger::shell::block_alloc::{AllocFailure, TxBin}; @@ -18,19 +18,10 @@ use crate::node::ledger::shims::abcipp_shim_types::shim::TxBytes; /// transaction numbers, in a block proposal. #[derive(Default)] pub struct ValidationMeta { - /// Space and gas utilized by encrypted txs. - pub encrypted_txs_bins: EncryptedTxsBins, - /// Vote extension digest counters. + /// Gas emitted by users. + pub user_gas: TxBin, /// Space utilized by all txs. pub txs_bin: TxBin, - /// Check if the decrypted tx queue has any elements - /// left. - /// - /// This field will only evaluate to true if a block - /// proposer didn't include all decrypted txs in a block. - pub decrypted_queue_has_remaining_txs: bool, - /// Check if a block has decrypted txs. - pub has_decrypted_txs: bool, } impl From<&WlState> for ValidationMeta @@ -43,15 +34,10 @@ where state.pos_queries().get_max_proposal_bytes().get(); let max_block_gas = namada::parameters::get_max_block_gas(state).unwrap(); - let encrypted_txs_bin = - EncryptedTxsBins::new(max_proposal_bytes, max_block_gas); + + let user_gas = TxBin::init(max_block_gas); let txs_bin = TxBin::init(max_proposal_bytes); - Self { - decrypted_queue_has_remaining_txs: false, - has_decrypted_txs: false, - encrypted_txs_bins: encrypted_txs_bin, - txs_bin, - } + Self { user_gas, txs_bin } } } @@ -94,7 +80,7 @@ where ) }; - let (tx_results, meta) = self.process_txs( + let tx_results = self.process_txs( &req.txs, req.time .expect("Missing timestamp in proposed block") @@ -121,22 +107,8 @@ where "Found invalid transactions, proposed block will be rejected" ); } - - let has_remaining_decrypted_txs = - meta.decrypted_queue_has_remaining_txs; - if has_remaining_decrypted_txs { - tracing::warn!( - proposer = ?HEXUPPER.encode(&req.proposer_address), - height = req.height, - hash = ?HEXUPPER.encode(&req.hash), - "Not all decrypted txs from the previous height were included in - the proposal, the block will be rejected" - ); - } - - let will_reject_proposal = invalid_txs || has_remaining_decrypted_txs; ( - if will_reject_proposal { + if invalid_txs { ProcessProposal::Reject } else { ProcessProposal::Accept @@ -157,10 +129,9 @@ where txs: &[TxBytes], block_time: DateTimeUtc, block_proposer: &Address, - ) -> (Vec, ValidationMeta) { - let mut tx_queue_iter = self.state.in_mem().tx_queue.iter(); + ) -> Vec { let mut temp_state = self.state.with_temp_write_log(); - let mut metadata = ValidationMeta::from(self.state.read_only()); + let mut metadata = ValidationMeta::from(&self.state.read_only()); let mut vp_wasm_cache = self.vp_wasm_cache.clone(); let mut tx_wasm_cache = self.tx_wasm_cache.clone(); @@ -169,7 +140,6 @@ where .map(|tx_bytes| { let result = self.check_proposal_tx( tx_bytes, - &mut tx_queue_iter, &mut metadata, &mut temp_state, block_time, @@ -192,10 +162,7 @@ where result }) .collect(); - metadata.decrypted_queue_has_remaining_txs = - !self.state.in_mem().tx_queue.is_empty() - && tx_queue_iter.next().is_some(); - (tx_results, metadata) + tx_results } /// Checks if the Tx can be deserialized from bytes. Checks the fees and @@ -221,10 +188,9 @@ where /// proposal is rejected (unless we can simply overwrite them in the /// next block). #[allow(clippy::too_many_arguments)] - pub fn check_proposal_tx<'a, CA>( + pub fn check_proposal_tx( &self, tx_bytes: &[u8], - tx_queue_iter: &mut impl Iterator, metadata: &mut ValidationMeta, temp_state: &mut TempWlState, block_time: DateTimeUtc, @@ -437,61 +403,7 @@ where }, } } - TxType::Decrypted(tx_header) => { - metadata.has_decrypted_txs = true; - match tx_queue_iter.next() { - Some(wrapper) => { - if wrapper.tx.raw_header_hash() != tx.raw_header_hash() - { - TxResult { - code: ResultCode::InvalidOrder.into(), - info: "Process proposal rejected a decrypted \ - transaction that violated the tx order \ - determined in the previous block" - .into(), - } - } else if matches!( - tx_header, - DecryptedTx::Undecryptable - ) { - // DKG is disabled, txs are not actually encrypted - TxResult { - code: ResultCode::InvalidTx.into(), - info: "The encrypted payload of tx was \ - incorrectly marked as un-decryptable" - .into(), - } - } else { - match tx.header().expiration { - Some(tx_expiration) - if block_time > tx_expiration => - { - TxResult { - code: ResultCode::ExpiredDecryptedTx - .into(), - info: format!( - "Tx expired at {:#?}, block time: \ - {:#?}", - tx_expiration, block_time - ), - } - } - _ => TxResult { - code: ResultCode::Ok.into(), - info: "Process Proposal accepted this \ - transaction" - .into(), - }, - } - } - } - None => TxResult { - code: ResultCode::ExtraTxs.into(), - info: "Received more decrypted txs than expected" - .into(), - }, - } - } + TxType::Decrypted(_) => unreachable!(), TxType::Wrapper(wrapper) => { // Account for gas and space. This is done even if the // transaction is later deemed invalid, to @@ -503,8 +415,8 @@ where // Account for the tx's resources even in case of an error. // Ignore any allocation error let _ = metadata - .encrypted_txs_bins - .try_dump(tx_bytes, u64::from(wrapper.gas_limit)); + .user_gas + .try_dump(u64::from(wrapper.gas_limit)); return TxResult { code: ResultCode::TxGasLimit.into(), @@ -513,34 +425,6 @@ where }; } - // try to allocate space and gas for this encrypted tx - if let Err(e) = metadata - .encrypted_txs_bins - .try_dump(tx_bytes, u64::from(wrapper.gas_limit)) - { - return TxResult { - code: ResultCode::AllocationError.into(), - info: e, - }; - } - // decrypted txs shouldn't show up before wrapper txs - if metadata.has_decrypted_txs { - return TxResult { - code: ResultCode::InvalidTx.into(), - info: "Decrypted txs should not be proposed before \ - wrapper txs" - .into(), - }; - } - if hints::unlikely(self.encrypted_txs_not_allowed()) { - return TxResult { - code: ResultCode::AllocationError.into(), - info: "Wrapper txs not allowed at the current block \ - height" - .into(), - }; - } - // ChainId check if tx_chain_id != self.chain_id { return TxResult { @@ -604,14 +488,6 @@ where ) -> shim::response::RevertProposal { Default::default() } - - /// Checks if it is not possible to include encrypted txs at the current - /// block height. - pub(super) fn encrypted_txs_not_allowed(&self) -> bool { - let is_2nd_height_off = self.is_deciding_offset_within_epoch(1); - let is_3rd_height_off = self.is_deciding_offset_within_epoch(2); - is_2nd_height_off || is_3rd_height_off - } } fn process_proposal_fee_check( @@ -1134,190 +1010,6 @@ mod test_process_proposal { ); } - /// Test that if the expected order of decrypted txs is - /// validated, [`process_proposal`] rejects it - #[test] - fn test_decrypted_txs_out_of_order() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); - let keypair = gen_keypair(); - let mut txs = vec![]; - for i in 0..3 { - let mut outer_tx = - Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native( - Amount::native_whole(i as u64), - ), - token: shell.state.in_mem().native_token.clone(), - }, - keypair.ref_to(), - Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), - None, - )))); - outer_tx.header.chain_id = shell.chain_id.clone(); - outer_tx - .set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); - outer_tx.set_data(Data::new( - format!("transaction data: {}", i).as_bytes().to_owned(), - )); - let gas_limit = - Gas::from(outer_tx.header().wrapper().unwrap().gas_limit) - .checked_sub(Gas::from(outer_tx.to_bytes().len() as u64)) - .unwrap(); - shell.enqueue_tx(outer_tx.clone(), gas_limit); - - outer_tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); - txs.push(outer_tx); - } - let response = { - let request = ProcessProposal { - txs: vec![ - txs[0].to_bytes(), - txs[2].to_bytes(), - txs[1].to_bytes(), - ], - }; - if let Err(TestError::RejectProposal(mut resp)) = - shell.process_proposal(request) - { - assert_eq!(resp.len(), 3); - resp.remove(1) - } else { - panic!("Test failed") - } - }; - assert_eq!(response.result.code, u32::from(ResultCode::InvalidOrder)); - assert_eq!( - response.result.info, - String::from( - "Process proposal rejected a decrypted transaction that \ - violated the tx order determined in the previous block" - ), - ); - } - - /// Test that a block containing a tx incorrectly labelled as undecryptable - /// is rejected by [`process_proposal`] - #[test] - fn test_incorrectly_labelled_as_undecryptable() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); - let keypair = gen_keypair(); - - let mut tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native( - Default::default(), - ), - token: shell.state.in_mem().native_token.clone(), - }, - keypair.ref_to(), - Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), - None, - )))); - tx.header.chain_id = shell.chain_id.clone(); - tx.set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); - tx.set_data(Data::new("transaction data".as_bytes().to_owned())); - let gas_limit = Gas::from(tx.header().wrapper().unwrap().gas_limit) - .checked_sub(Gas::from(tx.to_bytes().len() as u64)) - .unwrap(); - shell.enqueue_tx(tx.clone(), gas_limit); - - tx.header.tx_type = TxType::Decrypted(DecryptedTx::Undecryptable); - - let response = { - let request = ProcessProposal { - txs: vec![tx.to_bytes()], - }; - if let Err(TestError::RejectProposal(resp)) = - shell.process_proposal(request) - { - if let [resp] = resp.as_slice() { - resp.clone() - } else { - panic!("Test failed") - } - } else { - panic!("Test failed") - } - }; - assert_eq!(response.result.code, u32::from(ResultCode::InvalidTx)); - assert_eq!( - response.result.info, - String::from( - "The encrypted payload of tx was incorrectly marked as \ - un-decryptable" - ), - ) - } - - /// Test that if a wrapper tx contains marked undecryptable the proposal is - /// rejected - #[test] - fn test_undecryptable() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); - let keypair = crate::wallet::defaults::daewon_keypair(); - // not valid tx bytes - let wrapper = WrapperTx { - fee: Fee { - amount_per_gas_unit: DenominatedAmount::native( - Default::default(), - ), - token: shell.state.in_mem().native_token.clone(), - }, - pk: keypair.ref_to(), - epoch: Epoch(0), - gas_limit: GAS_LIMIT_MULTIPLIER.into(), - unshield_section_hash: None, - }; - - let tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); - let mut decrypted = tx.clone(); - decrypted.update_header(TxType::Decrypted(DecryptedTx::Undecryptable)); - - let gas_limit = Gas::from(tx.header().wrapper().unwrap().gas_limit) - .checked_sub(Gas::from(tx.to_bytes().len() as u64)) - .unwrap(); - shell.enqueue_tx(tx, gas_limit); - - let request = ProcessProposal { - txs: vec![decrypted.to_bytes()], - }; - shell.process_proposal(request).expect_err("Test failed"); - } - - /// Test that if more decrypted txs are submitted to - /// [`process_proposal`] than expected, they are rejected - #[test] - fn test_too_many_decrypted_txs() { - let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); - let mut tx = Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted)); - tx.header.chain_id = shell.chain_id.clone(); - tx.set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); - tx.set_data(Data::new("transaction data".as_bytes().to_owned())); - - let request = ProcessProposal { - txs: vec![tx.to_bytes()], - }; - let response = if let Err(TestError::RejectProposal(resp)) = - shell.process_proposal(request) - { - if let [resp] = resp.as_slice() { - resp.clone() - } else { - panic!("Test failed") - } - } else { - panic!("Test failed") - }; - assert_eq!(response.result.code, u32::from(ResultCode::ExtraTxs)); - assert_eq!( - response.result.info, - String::from("Received more decrypted txs than expected"), - ); - } - /// Process Proposal should reject a block containing a RawTx, but not panic #[test] fn test_raw_tx_rejected() { @@ -1704,55 +1396,6 @@ mod test_process_proposal { } } - /// Test that an expired decrypted transaction is marked as rejected but - /// still allows the block to be accepted - #[test] - fn test_expired_decrypted() { - let (mut shell, _recv, _, _) = test_utils::setup(); - let keypair = crate::wallet::defaults::daewon_keypair(); - - let mut wrapper = - Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native(1.into()), - token: shell.state.in_mem().native_token.clone(), - }, - keypair.ref_to(), - Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), - None, - )))); - wrapper.header.chain_id = shell.chain_id.clone(); - wrapper.header.expiration = Some(DateTimeUtc::default()); - wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); - wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); - wrapper.add_section(Section::Signature(Signature::new( - wrapper.sechashes(), - [(0, keypair)].into_iter().collect(), - None, - ))); - - shell.enqueue_tx(wrapper.clone(), GAS_LIMIT_MULTIPLIER.into()); - - let decrypted = - wrapper.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); - - // Run validation - let request = ProcessProposal { - txs: vec![decrypted.to_bytes()], - }; - match shell.process_proposal(request) { - Ok(txs) => { - assert_eq!(txs.len(), 1); - assert_eq!( - txs[0].result.code, - u32::from(ResultCode::ExpiredDecryptedTx) - ); - } - Err(_) => panic!("Test failed"), - } - } - /// Check that a tx requiring more gas than the block limit causes a block /// rejection #[test] @@ -2023,65 +1666,6 @@ mod test_process_proposal { } } - /// Test if we reject wrapper txs when they shouldn't be included in blocks. - /// - /// Currently, the conditions to reject wrapper - /// txs are simply to check if we are at the 2nd - /// or 3rd height offset within an epoch. - #[test] - fn test_include_only_protocol_txs() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(1u64); - let keypair = gen_keypair(); - let mut wrapper = - Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount_per_gas_unit: DenominatedAmount::native(0.into()), - token: shell.state.in_mem().native_token.clone(), - }, - keypair.ref_to(), - Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), - None, - )))); - wrapper.header.chain_id = shell.chain_id.clone(); - wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); - wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); - wrapper.add_section(Section::Signature(Signature::new( - wrapper.sechashes(), - [(0, keypair)].into_iter().collect(), - None, - ))); - let wrapper = wrapper.to_bytes(); - for height in [1u64, 2] { - if let Some(b) = shell.state.in_mem_mut().last_block.as_mut() { - b.height = height.into(); - } - let response = { - let request = ProcessProposal { - txs: vec![wrapper.clone()], - }; - if let Err(TestError::RejectProposal(mut resp)) = - shell.process_proposal(request) - { - assert_eq!(resp.len(), 1); - resp.remove(0) - } else { - panic!("Test failed") - } - }; - assert_eq!( - response.result.code, - u32::from(ResultCode::AllocationError) - ); - assert_eq!( - response.result.info, - String::from( - "Wrapper txs not allowed at the current block height" - ), - ); - } - } - /// Test max tx bytes parameter in ProcessProposal #[test] fn test_max_tx_bytes_process_proposal() { diff --git a/crates/apps/src/lib/node/ledger/shell/testing/node.rs b/crates/apps/src/lib/node/ledger/shell/testing/node.rs index f133082f8e..93d7778aaf 100644 --- a/crates/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/crates/apps/src/lib/node/ledger/shell/testing/node.rs @@ -501,26 +501,12 @@ impl MockNode { ); } - /// Advance to a block height that allows - /// txs - fn advance_to_allowed_block(&self) { - loop { - let not_allowed = - { self.shell.lock().unwrap().encrypted_txs_not_allowed() }; - if not_allowed { - self.finalize_and_commit(); - } else { - break; - } - } - } - /// Send a tx through Process Proposal and Finalize Block /// and register the results. pub fn submit_txs(&self, txs: Vec>) { // The block space allocator disallows encrypted txs in certain blocks. // Advance to block height that allows txs. - self.advance_to_allowed_block(); + self.finalize_and_commit(); let (proposer_address, votes) = self.prepare_request(); let time = DateTimeUtc::now(); @@ -972,7 +958,7 @@ fn parse_tm_query( query: namada::tendermint_rpc::query::Query, ) -> dumb_queries::QueryMatcher { const QUERY_PARSING_REGEX_STR: &str = - r"^tm\.event='NewBlock' AND (accepted|applied)\.hash='([^']+)'$"; + r"^tm\.event='NewBlock' AND (applied)\.hash='([^']+)'$"; lazy_static! { /// Compiled regular expression used to parse Tendermint queries. @@ -983,13 +969,10 @@ fn parse_tm_query( let captures = QUERY_PARSING_REGEX.captures(&query).unwrap(); match captures.get(0).unwrap().as_str() { - "accepted" => dumb_queries::QueryMatcher::accepted( - captures.get(1).unwrap().as_str().try_into().unwrap(), - ), "applied" => dumb_queries::QueryMatcher::applied( captures.get(1).unwrap().as_str().try_into().unwrap(), ), - _ => unreachable!("We only query accepted or applied txs"), + _ => unreachable!("We only query applied txs"), } } diff --git a/crates/apps/src/lib/node/ledger/shell/vote_extensions.rs b/crates/apps/src/lib/node/ledger/shell/vote_extensions.rs index 9bf9f9bd1b..1cfa899d0c 100644 --- a/crates/apps/src/lib/node/ledger/shell/vote_extensions.rs +++ b/crates/apps/src/lib/node/ledger/shell/vote_extensions.rs @@ -4,6 +4,7 @@ pub mod bridge_pool_vext; pub mod eth_events; pub mod val_set_update; +use drain_filter_polyfill::DrainFilter; use namada::ethereum_bridge::protocol::transactions::bridge_pool_roots::sign_bridge_pool_root; use namada::ethereum_bridge::protocol::transactions::ethereum_events::sign_ethereum_events; use namada::ethereum_bridge::protocol::transactions::validator_set_update::sign_validator_set_update; @@ -115,9 +116,10 @@ where /// ones we could deserialize to vote extension protocol txs. pub fn deserialize_vote_extensions<'shell>( &'shell self, - txs: &'shell [TxBytes], - ) -> impl Iterator + 'shell { - txs.iter().filter_map(move |tx_bytes| { + txs: &'shell mut Vec, + ) -> DrainFilter<'shell, TxBytes, impl FnMut(&mut TxBytes) -> bool + 'shell> + { + drain_filter_polyfill::VecExt::drain_filter(txs, move |tx_bytes| { let tx = match Tx::try_from(tx_bytes.as_ref()) { Ok(tx) => tx, Err(err) => { @@ -126,12 +128,12 @@ where "Failed to deserialize tx in \ deserialize_vote_extensions" ); - return None; + return false; } }; - match (&tx).try_into().ok()? { - EthereumTxData::BridgePoolVext(_) => Some(tx_bytes.clone()), - EthereumTxData::EthEventsVext(ext) => { + match (&tx).try_into().ok() { + Some(EthereumTxData::BridgePoolVext(_)) => true, + Some(EthereumTxData::EthEventsVext(ext)) => { // NB: only propose events with at least // one valid nonce ext.data @@ -144,7 +146,7 @@ where }) .then(|| tx_bytes.clone()) } - EthereumTxData::ValSetUpdateVext(ext) => { + Some(EthereumTxData::ValSetUpdateVext(ext)) => { // only include non-stale validator set updates // in block proposals. it might be sitting long // enough in the mempool for it to no longer be diff --git a/crates/apps/src/lib/node/ledger/shims/abcipp_shim.rs b/crates/apps/src/lib/node/ledger/shims/abcipp_shim.rs index 11788fe9d1..eaf929d4da 100644 --- a/crates/apps/src/lib/node/ledger/shims/abcipp_shim.rs +++ b/crates/apps/src/lib/node/ledger/shims/abcipp_shim.rs @@ -143,7 +143,7 @@ impl AbcippShim { proposer from tendermint raw hash", ); - let (processing_results, _) = self.service.process_txs( + let processing_results = self.service.process_txs( &self.delivered_txs, block_time, &block_proposer, diff --git a/crates/apps/src/lib/node/ledger/storage/rocksdb.rs b/crates/apps/src/lib/node/ledger/storage/rocksdb.rs index 18e2fa13f6..a329589b5e 100644 --- a/crates/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/crates/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -731,17 +731,6 @@ impl DB for RocksDB { return Ok(None); } }; - let tx_queue: TxQueue = match self - .0 - .get_cf(state_cf, "tx_queue") - .map_err(|e| Error::DBError(e.into_string()))? - { - Some(bytes) => decode(bytes).map_err(Error::CodingError)?, - None => { - tracing::error!("Couldn't load tx queue from the DB"); - return Ok(None); - } - }; let ethereum_height: Option = match self .0 @@ -890,7 +879,6 @@ impl DB for RocksDB { next_epoch_min_start_time, update_epoch_blocks_delay, address_gen, - tx_queue, ethereum_height, eth_events_queue, })), @@ -921,7 +909,6 @@ impl DB for RocksDB { address_gen, results, conversion_state, - tx_queue, ethereum_height, eth_events_queue, }: BlockStateWrite = state; @@ -1011,7 +998,6 @@ impl DB for RocksDB { // Write the predecessor value for rollback batch.0.put_cf(state_cf, "pred/tx_queue", pred_tx_queue); } - batch.0.put_cf(state_cf, "tx_queue", encode(&tx_queue)); batch .0 .put_cf(state_cf, "ethereum_height", encode(ðereum_height)); @@ -2258,7 +2244,6 @@ mod test { let next_epoch_min_start_time = DateTimeUtc::now(); let update_epoch_blocks_delay = None; let address_gen = EstablishedAddressGen::new("whatever"); - let tx_queue = TxQueue::default(); let results = BlockResults::default(); let eth_events_queue = EthEventsQueue::default(); let block = BlockStateWrite { @@ -2275,7 +2260,6 @@ mod test { next_epoch_min_start_time, update_epoch_blocks_delay, address_gen: &address_gen, - tx_queue: &tx_queue, ethereum_height: None, eth_events_queue: ð_events_queue, }; diff --git a/crates/benches/process_wrapper.rs b/crates/benches/process_wrapper.rs index da26796e78..fa2b7fa7fe 100644 --- a/crates/benches/process_wrapper.rs +++ b/crates/benches/process_wrapper.rs @@ -59,7 +59,6 @@ fn process_tx(c: &mut Criterion) { b.iter_batched( || { ( - shell.state.in_mem().tx_queue.clone(), // Prevent block out of gas and replay protection shell.state.with_temp_write_log(), ValidationMeta::from(shell.state.read_only()), @@ -69,7 +68,6 @@ fn process_tx(c: &mut Criterion) { ) }, |( - tx_queue, mut temp_state, mut validation_meta, mut vp_wasm_cache, @@ -81,7 +79,6 @@ fn process_tx(c: &mut Criterion) { shell .check_proposal_tx( &wrapper, - &mut tx_queue.iter(), &mut validation_meta, &mut temp_state, datetime, diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index dddb367f7c..71b0fd347b 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -1468,6 +1468,9 @@ pub struct IndexedTx { pub height: BlockHeight, /// The index in the block of the tx pub index: TxIndex, + /// A transcation can have up to two sheilded transfers. + /// This indicates if the wrapper contained a sheilded transfer. + pub is_wrapper: bool, } #[cfg(test)] diff --git a/crates/namada/src/ledger/native_vp/masp.rs b/crates/namada/src/ledger/native_vp/masp.rs index 874bb6a464..690839b7fb 100644 --- a/crates/namada/src/ledger/native_vp/masp.rs +++ b/crates/namada/src/ledger/native_vp/masp.rs @@ -267,9 +267,12 @@ where .ctx .read_post::(pin_keys.first().unwrap())? { - Some(IndexedTx { height, index }) - if height == self.ctx.get_block_height()? - && index == self.ctx.get_tx_index()? => {} + Some(IndexedTx { + height, + index, + is_wrapper: false, + }) if height == self.ctx.get_block_height()? + && index == self.ctx.get_tx_index()? => {} Some(_) => { return Err(Error::NativeVpError( native_vp::Error::SimpleMessage( diff --git a/crates/namada/src/ledger/protocol/mod.rs b/crates/namada/src/ledger/protocol/mod.rs index 6a7991ef65..7641fc77d0 100644 --- a/crates/namada/src/ledger/protocol/mod.rs +++ b/crates/namada/src/ledger/protocol/mod.rs @@ -13,9 +13,7 @@ use namada_gas::TxGasMeter; use namada_sdk::tx::TX_TRANSFER_WASM; use namada_state::StorageWrite; use namada_tx::data::protocol::ProtocolTxType; -use namada_tx::data::{ - DecryptedTx, GasLimit, TxResult, TxType, VpsResult, WrapperTx, -}; +use namada_tx::data::{GasLimit, TxResult, TxType, VpsResult, WrapperTx}; use namada_tx::{Section, Tx}; use namada_vote_ext::EthereumTxData; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; @@ -174,8 +172,7 @@ where CA: 'static + WasmCacheAccess + Sync, { match tx.header().tx_type { - TxType::Raw => Err(Error::TxTypeError), - TxType::Decrypted(DecryptedTx::Decrypted) => apply_wasm_tx( + TxType::Raw => apply_wasm_tx( tx, &tx_index, ShellParams { @@ -192,7 +189,7 @@ where let fee_unshielding_transaction = get_fee_unshielding_transaction(&tx, wrapper); let changed_keys = apply_wrapper_tx( - tx, + tx.clone(), wrapper, fee_unshielding_transaction, tx_bytes, @@ -204,18 +201,20 @@ where }, wrapper_args, )?; - Ok(TxResult { - gas_used: tx_gas_meter.borrow().get_tx_consumed_gas(), - changed_keys, - vps_result: VpsResult::default(), - initialized_accounts: vec![], - ibc_events: BTreeSet::default(), - eth_bridge_events: BTreeSet::default(), - }) - } - TxType::Decrypted(DecryptedTx::Undecryptable) => { - Ok(TxResult::default()) + let mut inner_res = apply_wasm_tx( + tx, + &tx_index, + ShellParams { + tx_gas_meter, + wl_storage, + vp_wasm_cache, + tx_wasm_cache, + }, + )?; + inner_res.wrapper_changed_keys = changed_keys; + Ok(inner_res) } + _ => Ok(TxResult::default()), } } @@ -618,6 +617,7 @@ where Ok(TxResult { gas_used, + wrapper_changed_keys: Default::default(), changed_keys, vps_result, initialized_accounts, @@ -633,7 +633,7 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - if let TxType::Decrypted(DecryptedTx::Decrypted) = tx.header().tx_type { + if let TxType::Wrapper(_) = tx.header().tx_type { if let Some(code_sec) = tx .get_section(tx.code_sechash()) .and_then(|x| Section::code_sec(&x)) @@ -1193,7 +1193,6 @@ mod tests { let (mut state, _validators) = test_utils::setup_default_storage(); let mut tx = Tx::new(ChainId::default(), None); - tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); // pseudo-random code hash let code = vec![1_u8, 2, 3]; let tx_hash = Hash::sha256(&code); diff --git a/crates/sdk/src/events/log.rs b/crates/sdk/src/events/log.rs index 3eb19f89e7..f68d27b469 100644 --- a/crates/sdk/src/events/log.rs +++ b/crates/sdk/src/events/log.rs @@ -91,16 +91,16 @@ mod tests { "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"; /// An accepted tx hash query. - macro_rules! accepted { + macro_rules! applied { ($hash:expr) => { - dumb_queries::QueryMatcher::accepted(Hash::try_from($hash).unwrap()) + dumb_queries::QueryMatcher::applied(Hash::try_from($hash).unwrap()) }; } /// Return a vector of mock `FinalizeBlock` events. fn mock_tx_events(hash: &str) -> Vec { let event_1 = Event { - event_type: EventType::Accepted, + event_type: EventType::Applied, level: EventLevel::Block, attributes: { let mut attrs = std::collections::HashMap::new(); @@ -109,7 +109,7 @@ mod tests { }, }; let event_2 = Event { - event_type: EventType::Applied, + event_type: EventType::Proposal, level: EventLevel::Block, attributes: { let mut attrs = std::collections::HashMap::new(); @@ -137,7 +137,7 @@ mod tests { // inspect log let events_in_log: Vec<_> = - log.iter_with_matcher(accepted!(HASH)).cloned().collect(); + log.iter_with_matcher(applied!(HASH)).cloned().collect(); assert_eq!(events_in_log.len(), NUM_HEIGHTS); @@ -176,7 +176,7 @@ mod tests { // inspect log - it should be full let events_in_log: Vec<_> = - log.iter_with_matcher(accepted!(HASH)).cloned().collect(); + log.iter_with_matcher(applied!(HASH)).cloned().collect(); assert_eq!(events_in_log.len(), MATCHED_EVENTS); @@ -184,12 +184,12 @@ mod tests { assert_eq!(events[0], event); } - // add a new APPLIED event to the log, - // pruning the first ACCEPTED event we added + // add a new PROPOSAL event to the log, + // pruning the first APPLIED event we added log.log_events(Some(events[1].clone())); let events_in_log: Vec<_> = - log.iter_with_matcher(accepted!(HASH)).cloned().collect(); + log.iter_with_matcher(applied!(HASH)).cloned().collect(); const ACCEPTED_EVENTS: usize = MATCHED_EVENTS - 1; assert_eq!(events_in_log.len(), ACCEPTED_EVENTS); diff --git a/crates/sdk/src/events/log/dumb_queries.rs b/crates/sdk/src/events/log/dumb_queries.rs index 1d2b0527a2..2d2896b3da 100644 --- a/crates/sdk/src/events/log/dumb_queries.rs +++ b/crates/sdk/src/events/log/dumb_queries.rs @@ -41,16 +41,6 @@ impl QueryMatcher { }) } - /// Returns a query matching the given accepted transaction hash. - pub fn accepted(tx_hash: Hash) -> Self { - let mut attributes = HashMap::new(); - attributes.insert("hash".to_string(), tx_hash.to_string()); - Self { - event_type: EventType::Accepted, - attributes, - } - } - /// Returns a query matching the given applied transaction hash. pub fn applied(tx_hash: Hash) -> Self { let mut attributes = HashMap::new(); @@ -132,13 +122,13 @@ mod tests { let mut attributes = HashMap::new(); attributes.insert("hash".to_string(), HASH.to_string()); let matcher = QueryMatcher { - event_type: EventType::Accepted, + event_type: EventType::Proposal, attributes, }; let tests = { let event_1 = Event { - event_type: EventType::Accepted, + event_type: EventType::Proposal, level: EventLevel::Block, attributes: { let mut attrs = std::collections::HashMap::new(); diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index 35315ea39a..22ec3fa58c 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -150,6 +150,13 @@ pub enum TransferErr { General(#[from] Error), } +#[derive(Debug, Clone)] +struct ExtractedMaspTx { + fee_unshielding: + Option<(BTreeSet, Transaction)>, + inner_tx: Option<(BTreeSet, Transaction)>, +} + /// MASP verifying keys pub struct PVKs { /// spend verifying key @@ -844,21 +851,36 @@ impl ShieldedContext { for (idx, tx_event) in txs_results { let tx = Tx::try_from(block[idx.0 as usize].as_ref()) .map_err(|e| Error::Other(e.to_string()))?; - let (changed_keys, masp_transaction) = Self::extract_masp_tx( + let ExtractedMaspTx { + fee_unshielding, + inner_tx, + } = Self::extract_masp_tx( &tx, ExtractShieldedActionArg::Event::(&tx_event), true, ) .await?; - - // Collect the current transaction - shielded_txs.insert( - IndexedTx { - height: height.into(), - index: idx, - }, - (epoch, changed_keys, masp_transaction), - ); + // Collect the current transaction(s) + fee_unshielding.and_then(|(changed_keys, masp_transaction)| { + shielded_txs.insert( + IndexedTx { + height: height.into(), + index: idx, + is_wrapper: true, + }, + (epoch, changed_keys, masp_transaction), + ) + }); + inner_tx.and_then(|(changed_keys, masp_transaction)| { + shielded_txs.insert( + IndexedTx { + height: height.into(), + index: idx, + is_wrapper: false, + }, + (epoch, changed_keys, masp_transaction), + ) + }); } } @@ -870,88 +892,72 @@ impl ShieldedContext { tx: &Tx, action_arg: ExtractShieldedActionArg<'args, C>, check_header: bool, - ) -> Result<(BTreeSet, Transaction), Error> { - let maybe_transaction = if check_header { - let tx_header = tx.header(); - // NOTE: simply looking for masp sections attached to the tx - // is not safe. We don't validate the sections attached to a - // transaction se we could end up with transactions carrying - // an unnecessary masp section. We must instead look for the - // required masp sections in the signed commitments (hashes) - // of the transactions' headers/data sections - if let Some(wrapper_header) = tx_header.wrapper() { - let hash = - wrapper_header.unshield_section_hash.ok_or_else(|| { - Error::Other( - "Missing expected fee unshielding section hash" - .to_string(), - ) - })?; - - let masp_transaction = tx - .get_section(&hash) - .ok_or_else(|| { - Error::Other( - "Missing expected masp section".to_string(), - ) - })? - .masp_tx() - .ok_or_else(|| { - Error::Other("Missing masp transaction".to_string()) - })?; - - // We use the changed keys instead of the Transfer object - // because those are what the masp validity predicate works on - let changed_keys = - if let ExtractShieldedActionArg::Event(tx_event) = - action_arg - { - let tx_result_str = tx_event - .attributes - .iter() - .find_map(|attr| { - if attr.key == "inner_tx" { - Some(&attr.value) - } else { - None - } - }) - .ok_or_else(|| { - Error::Other( - "Missing required tx result in event" - .to_string(), - ) - })?; - TxResult::from_str(tx_result_str) - .map_err(|e| Error::Other(e.to_string()))? - .changed_keys + ) -> Result { + // We use the changed keys instead of the Transfer object + // because those are what the masp validity predicate works on + let TxResult { + wrapper_changed_keys, + changed_keys, + .. + } = if let ExtractShieldedActionArg::Event(tx_event) = action_arg { + let tx_result_str = tx_event + .attributes + .iter() + .find_map(|attr| { + if attr.key == "inner_tx" { + Some(&attr.value) } else { - BTreeSet::default() - }; + None + } + }) + .ok_or_else(|| { + Error::Other( + "Missing required tx result in event".to_string(), + ) + })?; + TxResult::from_str(tx_result_str) + .map_err(|e| Error::Other(e.to_string()))? + } else { + panic!("Expected a event type Shield action argument.") + }; - Some((changed_keys, masp_transaction)) - } else { - None - } + let tx_header = tx.header(); + // NOTE: simply looking for masp sections attached to the tx + // is not safe. We don't validate the sections attached to a + // transaction se we could end up with transactions carrying + // an unnecessary masp section. We must instead look for the + // required masp sections in the signed commitments (hashes) + // of the transactions' headers/data sections + let wrapper_header = tx_header + .wrapper() + .expect("All transactions must have a wrapper"); + let maybe_fee_unshield = if let (Some(hash), true) = + (wrapper_header.unshield_section_hash, check_header) + { + let masp_transaction = tx + .get_section(&hash) + .ok_or_else(|| { + Error::Other("Missing expected masp section".to_string()) + })? + .masp_tx() + .ok_or_else(|| { + Error::Other("Missing masp transaction".to_string()) + })?; + + Some((wrapper_changed_keys, masp_transaction)) } else { None }; - let result = if let Some(tx) = maybe_transaction { - tx - } else { - // Expect decrypted transaction - let tx_data = tx.data().ok_or_else(|| { - Error::Other("Missing data section".to_string()) - })?; - match Transfer::try_from_slice(&tx_data) { - Ok(transfer) => { - let masp_transaction = tx - .get_section(&transfer.shielded.ok_or_else(|| { - Error::Other( - "Missing masp section hash".to_string(), - ) - })?) + // Expect transaction + let tx_data = tx + .data() + .ok_or_else(|| Error::Other("Missing data section".to_string()))?; + let maybe_masp_tx = match Transfer::try_from_slice(&tx_data) { + Ok(transfer) => { + if let Some(hash) = transfer.shielded { + let masp_tx = tx + .get_section(&hash) .ok_or_else(|| { Error::Other( "Missing masp section in transaction" @@ -963,50 +969,25 @@ impl ShieldedContext { Error::Other("Missing masp transaction".to_string()) })?; - // We use the changed keys instead of the Transfer object - // because those are what the masp validity predicate works - // on - let changed_keys = - if let ExtractShieldedActionArg::Event(tx_event) = - action_arg - { - let tx_result_str = tx_event - .attributes - .iter() - .find_map(|attr| { - if attr.key == "inner_tx" { - Some(&attr.value) - } else { - None - } - }) - .ok_or_else(|| { - Error::Other( - "Missing required tx result in event" - .to_string(), - ) - })?; - TxResult::from_str(tx_result_str) - .map_err(|e| Error::Other(e.to_string()))? - .changed_keys - } else { - BTreeSet::default() - }; - (changed_keys, masp_transaction) - } - Err(_) => { - // This should be a MASP over IBC transaction, it - // could be a ShieldedTransfer or an Envelope - // message, need to try both - - extract_payload_from_shielded_action::( - &tx_data, action_arg, - ) - .await? + Some((changed_keys, masp_tx)) + } else { + None } } + Err(_) => { + // This should be a MASP over IBC transaction, it + // could be a ShieldedTransfer or an Envelope + // message, need to try both + extract_payload_from_shielded_action::(&tx_data, action_arg) + .await + .ok() + } }; - Ok(result) + + Ok(ExtractedMaspTx { + fee_unshielding: maybe_fee_unshield, + inner_tx: maybe_masp_tx, + }) } /// Applies the given transaction to the supplied context. More precisely, @@ -1768,7 +1749,11 @@ impl ShieldedContext { )), false, ) - .await?; + .await? + .inner_tx + .ok_or_else(|| { + Error::Other("Missing shielded inner portion of pinned tx".into()) + })?; // Accumulate the combined output note value into this Amount let mut val_acc = I128Sum::zero(); @@ -2445,9 +2430,14 @@ impl ShieldedContext { let idx = TxIndex(response_tx.index); // Only process yet unprocessed transactions which have // been accepted by node VPs - let should_process = !transfers - .contains_key(&IndexedTx { height, index: idx }) - && block_results[u64::from(height) as usize] + // TODO: Check that wrappers shouldn't be considered + // here + let should_process = + !transfers.contains_key(&IndexedTx { + height, + index: idx, + is_wrapper: false, + }) && block_results[u64::from(height) as usize] .is_accepted(idx.0 as usize); if !should_process { continue; @@ -2482,7 +2472,11 @@ impl ShieldedContext { // No shielded accounts are affected by this // Transfer transfers.insert( - IndexedTx { height, index: idx }, + IndexedTx { + height, + index: idx, + is_wrapper: false, + }, (epoch, delta, TransactionDelta::new()), ); } diff --git a/crates/sdk/src/queries/shell.rs b/crates/sdk/src/queries/shell.rs index d4bcca3409..94fd40e36b 100644 --- a/crates/sdk/src/queries/shell.rs +++ b/crates/sdk/src/queries/shell.rs @@ -98,9 +98,6 @@ router! {SHELL, // Block results access - read bit-vec ( "results" ) -> Vec = read_results, - // was the transaction accepted? - ( "accepted" / [tx_hash: Hash] ) -> Option = accepted, - // was the transaction applied? ( "applied" / [tx_hash: Hash] ) -> Option = applied, @@ -507,23 +504,6 @@ where Ok(data) } -fn accepted( - ctx: RequestCtx<'_, D, H, V, T>, - tx_hash: Hash, -) -> namada_storage::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - let matcher = dumb_queries::QueryMatcher::accepted(tx_hash); - Ok(ctx - .event_log - .iter_with_matcher(matcher) - .by_ref() - .next() - .cloned()) -} - fn applied( ctx: RequestCtx<'_, D, H, V, T>, tx_hash: Hash, diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index 9b7de172fb..85bf82f19a 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -103,9 +103,6 @@ pub async fn query_tx_status( "Transaction status query deadline of {deadline:?} exceeded" ); match status { - TxEventQuery::Accepted(_) => { - Error::Tx(TxSubmitError::AcceptTimeout) - } TxEventQuery::Applied(_) => { Error::Tx(TxSubmitError::AppliedTimeout) } @@ -460,8 +457,6 @@ pub async fn query_has_storage_key( /// Represents a query for an event pertaining to the specified transaction #[derive(Debug, Copy, Clone)] pub enum TxEventQuery<'a> { - /// Queries whether transaction with given hash was accepted - Accepted(&'a str), /// Queries whether transaction with given hash was applied Applied(&'a str), } @@ -470,7 +465,6 @@ impl<'a> TxEventQuery<'a> { /// The event type to which this event query pertains pub fn event_type(self) -> &'static str { match self { - TxEventQuery::Accepted(_) => "accepted", TxEventQuery::Applied(_) => "applied", } } @@ -478,7 +472,6 @@ impl<'a> TxEventQuery<'a> { /// The transaction to which this event query pertains pub fn tx_hash(self) -> &'a str { match self { - TxEventQuery::Accepted(tx_hash) => tx_hash, TxEventQuery::Applied(tx_hash) => tx_hash, } } @@ -488,9 +481,6 @@ impl<'a> TxEventQuery<'a> { impl<'a> From> for Query { fn from(tx_query: TxEventQuery<'a>) -> Self { match tx_query { - TxEventQuery::Accepted(tx_hash) => { - Query::default().and_eq("accepted.hash", tx_hash) - } TxEventQuery::Applied(tx_hash) => { Query::default().and_eq("applied.hash", tx_hash) } @@ -506,15 +496,9 @@ pub async fn query_tx_events( ) -> std::result::Result, ::Error> { let tx_hash: Hash = tx_event_query.tx_hash().try_into().unwrap(); match tx_event_query { - TxEventQuery::Accepted(_) => { - RPC.shell().accepted(client, &tx_hash).await - } - /*.wrap_err_with(|| { - eyre!("Failed querying whether a transaction was accepted") - })*/, - TxEventQuery::Applied(_) => RPC.shell().applied(client, &tx_hash).await, /*.wrap_err_with(|| { - eyre!("Error querying whether a transaction was applied") - })*/ + TxEventQuery::Applied(_) => RPC.shell().applied(client, &tx_hash).await, /* .wrap_err_with(|| { + * eyre!("Error querying whether a transaction was applied") + * }) */ } } @@ -561,10 +545,8 @@ pub enum TxBroadcastData { Live { /// Transaction to broadcast tx: Tx, - /// Hash of the wrapper transaction - wrapper_hash: String, - /// Hash of decrypted transaction - decrypted_hash: String, + /// Hash of the transaction + tx_hash: String, }, } diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 15b69575d0..53ea588f1e 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -220,15 +220,10 @@ pub async fn process_tx( expect_dry_broadcast(TxBroadcastData::DryRun(tx), context).await } else { // We use this to determine when the wrapper tx makes it on-chain - let wrapper_hash = tx.header_hash().to_string(); + let tx_hash = tx.header_hash().to_string(); // We use this to determine when the decrypted inner tx makes it // on-chain - let decrypted_hash = tx.raw_header_hash().to_string(); - let to_broadcast = TxBroadcastData::Live { - tx, - wrapper_hash, - decrypted_hash, - }; + let to_broadcast = TxBroadcastData::Live { tx, tx_hash }; // TODO: implement the code to resubmit the wrapper if it fails because // of masp epoch Either broadcast or submit transaction and // collect result into sum type @@ -313,12 +308,8 @@ pub async fn broadcast_tx( context: &impl Namada, to_broadcast: &TxBroadcastData, ) -> Result { - let (tx, wrapper_tx_hash, decrypted_tx_hash) = match to_broadcast { - TxBroadcastData::Live { - tx, - wrapper_hash, - decrypted_hash, - } => Ok((tx, wrapper_hash, decrypted_hash)), + let (tx, tx_hash) = match to_broadcast { + TxBroadcastData::Live { tx, tx_hash } => Ok((tx, tx_hash)), TxBroadcastData::DryRun(tx) => { Err(TxSubmitError::ExpectLiveRun(tx.clone())) } @@ -342,14 +333,7 @@ pub async fn broadcast_tx( // Print the transaction identifiers to enable the extraction of // acceptance/application results later { - display_line!( - context.io(), - "Wrapper transaction hash: {wrapper_tx_hash}", - ); - display_line!( - context.io(), - "Inner transaction hash: {decrypted_tx_hash}", - ); + display_line!(context.io(), "Transaction hash: {tx_hash}",); } Ok(response) } else { @@ -373,12 +357,8 @@ pub async fn submit_tx( context: &impl Namada, to_broadcast: TxBroadcastData, ) -> Result { - let (_, wrapper_hash, decrypted_hash) = match &to_broadcast { - TxBroadcastData::Live { - tx, - wrapper_hash, - decrypted_hash, - } => Ok((tx, wrapper_hash, decrypted_hash)), + let (_, tx_hash) = match &to_broadcast { + TxBroadcastData::Live { tx, tx_hash } => Ok((tx, tx_hash)), TxBroadcastData::DryRun(tx) => { Err(TxSubmitError::ExpectLiveRun(tx.clone())) } @@ -398,36 +378,12 @@ pub async fn submit_tx( "Awaiting transaction approval", ); - let response = { - let wrapper_query = rpc::TxEventQuery::Accepted(wrapper_hash.as_str()); - let event = - rpc::query_tx_status(context, wrapper_query, deadline).await?; - let wrapper_resp = TxResponse::from_event(event); - - if display_wrapper_resp_and_get_result(context, &wrapper_resp) { - display_line!( - context.io(), - "Waiting for inner transaction result..." - ); - // The transaction is now on chain. We wait for it to be decrypted - // and applied - // We also listen to the event emitted when the encrypted - // payload makes its way onto the blockchain - let decrypted_query = - rpc::TxEventQuery::Applied(decrypted_hash.as_str()); - let event = - rpc::query_tx_status(context, decrypted_query, deadline) - .await?; - let inner_resp = TxResponse::from_event(event); - - display_inner_resp(context, &inner_resp); - Ok(inner_resp) - } else { - Ok(wrapper_resp) - } - }; - - response + // The transaction is now on chain. We wait for it to be applied + let tx_query = rpc::TxEventQuery::Applied(tx_hash.as_str()); + let event = rpc::query_tx_status(context, tx_query, deadline).await?; + let response = TxResponse::from_event(event); + display_inner_resp(context, &response); + Ok(response) } /// Display a result of a wrapper tx. @@ -2983,11 +2939,9 @@ async fn expect_dry_broadcast( let result = rpc::dry_run_tx(context, tx.to_bytes()).await?; Ok(ProcessTxResponse::DryRun(result)) } - TxBroadcastData::Live { - tx, - wrapper_hash: _, - decrypted_hash: _, - } => Err(Error::from(TxSubmitError::ExpectDryRun(tx))), + TxBroadcastData::Live { tx, tx_hash: _ } => { + Err(Error::from(TxSubmitError::ExpectDryRun(tx))) + } } } diff --git a/crates/shielded_token/src/utils.rs b/crates/shielded_token/src/utils.rs index 42fc6413dd..b4253e76e2 100644 --- a/crates/shielded_token/src/utils.rs +++ b/crates/shielded_token/src/utils.rs @@ -76,6 +76,7 @@ pub fn handle_masp_tx( IndexedTx { height: ctx.get_block_height()?, index: ctx.get_tx_index()?, + is_wrapper: false, }, )?; } diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index afe728c4fa..e8aaf8df9d 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -632,7 +632,6 @@ pub mod testing { update_epoch_blocks_delay: None, tx_index: TxIndex::default(), conversion_state: ConversionState::default(), - tx_queue: TxQueue::default(), expired_txs_queue: ExpiredTxsQueue::default(), native_token: address::testing::nam(), ethereum_height: None, diff --git a/crates/storage/src/db.rs b/crates/storage/src/db.rs index 5ce22e85db..a8d51f7da4 100644 --- a/crates/storage/src/db.rs +++ b/crates/storage/src/db.rs @@ -15,7 +15,6 @@ use namada_merkle_tree::{ use thiserror::Error; use crate::conversion_state::ConversionState; -use crate::tx_queue::TxQueue; #[allow(missing_docs)] #[derive(Error, Debug)] @@ -69,8 +68,6 @@ pub struct BlockStateRead { pub results: BlockResults, /// The conversion state pub conversion_state: ConversionState, - /// Wrapper txs to be decrypted in the next block proposal - pub tx_queue: TxQueue, /// The latest block height on Ethereum processed, if /// the bridge is enabled. pub ethereum_height: Option, @@ -106,8 +103,6 @@ pub struct BlockStateWrite<'a> { pub results: &'a BlockResults, /// The conversion state pub conversion_state: &'a ConversionState, - /// Wrapper txs to be decrypted in the next block proposal - pub tx_queue: &'a TxQueue, /// The latest block height on Ethereum processed, if /// the bridge is enabled. pub ethereum_height: Option<&'a ethereum_structs::BlockHeight>, diff --git a/crates/storage/src/mockdb.rs b/crates/storage/src/mockdb.rs index 4128321e54..e1943f6749 100644 --- a/crates/storage/src/mockdb.rs +++ b/crates/storage/src/mockdb.rs @@ -24,7 +24,6 @@ use crate::conversion_state::ConversionState; use crate::db::{ BlockStateRead, BlockStateWrite, DBIter, DBWriteBatch, Error, Result, DB, }; -use crate::tx_queue::TxQueue; use crate::types::{KVBytes, PrefixIterator}; const SUBSPACE_CF: &str = "subspace"; @@ -98,10 +97,6 @@ impl DB for MockDB { Some(bytes) => decode(bytes).map_err(Error::CodingError)?, None => return Ok(None), }; - let tx_queue: TxQueue = match self.0.borrow().get("tx_queue") { - Some(bytes) => decode(bytes).map_err(Error::CodingError)?, - None => return Ok(None), - }; let ethereum_height: Option = match self.0.borrow().get("ethereum_height") { @@ -214,7 +209,6 @@ impl DB for MockDB { address_gen, results, conversion_state, - tx_queue, ethereum_height, eth_events_queue, })), @@ -247,7 +241,6 @@ impl DB for MockDB { conversion_state, ethereum_height, eth_events_queue, - tx_queue, }: BlockStateWrite = state; // Epoch start height and time @@ -269,9 +262,6 @@ impl DB for MockDB { self.0 .borrow_mut() .insert("eth_events_queue".into(), encode(ð_events_queue)); - self.0 - .borrow_mut() - .insert("tx_queue".into(), encode(&tx_queue)); self.0 .borrow_mut() .insert("conversion_state".into(), encode(conversion_state)); diff --git a/crates/tx/src/data/decrypted.rs b/crates/tx/src/data/decrypted.rs index d764f03d00..99c19bc6d9 100644 --- a/crates/tx/src/data/decrypted.rs +++ b/crates/tx/src/data/decrypted.rs @@ -17,7 +17,6 @@ pub mod decrypted_tx { serde::Serialize, serde::Deserialize, )] - #[allow(clippy::large_enum_variant)] /// Holds the result of attempting to decrypt /// a transaction and the data necessary for /// other validators to verify diff --git a/crates/tx/src/data/mod.rs b/crates/tx/src/data/mod.rs index e187bdd9fb..db628d11e4 100644 --- a/crates/tx/src/data/mod.rs +++ b/crates/tx/src/data/mod.rs @@ -175,6 +175,8 @@ pub fn hash_tx(tx_bytes: &[u8]) -> Hash { pub struct TxResult { /// Total gas used by the transaction (includes the gas used by VPs) pub gas_used: Gas, + /// Storage keys touched by the wrapper transaction + pub wrapper_changed_keys: BTreeSet, /// Storage keys touched by the transaction pub changed_keys: BTreeSet, /// The results of all the triggered validity predicates by the transaction From 61484a4034f819417d2cc81df5fd2a6bd5e637f1 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 15 Feb 2024 11:32:21 +0100 Subject: [PATCH 02/17] [fix]: unit test now passing --- .../src/lib/node/ledger/shell/block_alloc.rs | 39 ++- .../shell/block_alloc/states/normal_txs.rs | 2 +- .../shell/block_alloc/states/protocol_txs.rs | 2 +- .../lib/node/ledger/shell/finalize_block.rs | 249 ++++++++++-------- .../lib/node/ledger/shell/process_proposal.rs | 14 +- .../src/lib/node/ledger/storage/rocksdb.rs | 12 - crates/namada/src/ledger/protocol/mod.rs | 22 +- crates/state/src/wl_storage.rs | 0 crates/state/src/write_log.rs | 10 +- crates/test_utils/src/lib.rs | 2 + wasm_for_tests/tx_invalid_data.wasm | Bin 0 -> 423715 bytes wasm_for_tests/wasm_source/Cargo.toml | 1 + wasm_for_tests/wasm_source/Makefile | 1 + wasm_for_tests/wasm_source/src/lib.rs | 15 ++ 14 files changed, 211 insertions(+), 158 deletions(-) create mode 100644 crates/state/src/wl_storage.rs create mode 100755 wasm_for_tests/tx_invalid_data.wasm diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index a8ba599f1f..5ce22be212 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -180,7 +180,10 @@ impl BlockAllocator { _state: PhantomData, block: TxBin::init(max), protocol_txs: TxBin::default(), - normal_txs: NormalTxsBins::new(max_block_gas), + normal_txs: NormalTxsBins{ + space: TxBin::init(tendermint_max_block_space_in_bytes), + gas: TxBin::init(max_block_gas), + }, } } } @@ -193,9 +196,9 @@ impl BlockAllocator { /// block space for a given round and the sum of the allotted space /// to each [`TxBin`] instance in a [`BlockAllocator`]. #[inline] - fn uninitialized_space_in_bytes(&self) -> u64 { + fn unoccupied_space_in_bytes(&self) -> u64 { let total_bin_space = - self.protocol_txs.allotted + self.normal_txs.space.allotted; + self.protocol_txs.occupied + self.normal_txs.space.occupied; self.block.allotted - total_bin_space } } @@ -492,9 +495,9 @@ mod tests { tendermint_max_block_space_in_bytes, 1_000, ); - let expected = tendermint_max_block_space_in_bytes - - threshold::ONE_HALF.over(tendermint_max_block_space_in_bytes); - assert_eq!(expected, bins.uninitialized_space_in_bytes()); + let expected = tendermint_max_block_space_in_bytes; + assert_eq!(bins.protocol_txs.allotted, threshold::ONE_HALF.over(tendermint_max_block_space_in_bytes)); + assert_eq!(expected, bins.unoccupied_space_in_bytes()); } /// Implementation of [`test_tx_dump_doesnt_fill_up_bin`]. @@ -518,12 +521,13 @@ mod tests { )); let mut protocol_tx_iter = protocol_txs.iter(); let mut allocated_txs = vec![]; + let mut new_size = 0; for tx in protocol_tx_iter.by_ref() { let bin = bins.borrow().protocol_txs; - let new_size = bin.occupied + tx.len() as u64; - if new_size >= bin.allotted { + if new_size + tx.len() as u64 >= bin.allotted { break; } else { + new_size += tx.len() as u64; allocated_txs.push(tx); } } @@ -532,11 +536,17 @@ mod tests { } let bins = RefCell::new(bins.into_inner().next_state()); - let decrypted_txs = normal_txs.into_iter().take_while(|tx| { + let mut new_size = bins.borrow().normal_txs.space.allotted; + let mut decrypted_txs = vec![]; + for tx in normal_txs { let bin = bins.borrow().normal_txs.space; - let new_size = bin.occupied + tx.len() as u64; - new_size < bin.allotted - }); + if (new_size + tx.len() as u64) < bin.allotted { + new_size += tx.len() as u64; + decrypted_txs.push(tx); + } else { + break; + } + } for tx in decrypted_txs { assert!( bins.borrow_mut() @@ -547,12 +557,13 @@ mod tests { let bins = RefCell::new(bins.into_inner().next_state()); let mut allocated_txs = vec![]; + let mut new_size = bins.borrow().protocol_txs.allotted; for tx in protocol_tx_iter.by_ref() { let bin = bins.borrow().protocol_txs; - let new_size = bin.occupied + tx.len() as u64; - if new_size >= bin.allotted { + if new_size + tx.len() as u64 >= bin.allotted { break; } else { + new_size += tx.len() as u64; allocated_txs.push(tx); } } diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs index b024333367..680b06a8dc 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs @@ -25,7 +25,7 @@ impl NextStateImpl for BlockAllocator { #[inline] fn next_state_impl(mut self) -> Self::Next { - let remaining_free_space = self.uninitialized_space_in_bytes(); + let remaining_free_space = self.unoccupied_space_in_bytes(); self.protocol_txs = TxBin::init(remaining_free_space); // cast state let Self { diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs index 97b642daff..570130b208 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs @@ -25,7 +25,7 @@ impl NextStateImpl for BlockAllocator> { #[inline] fn next_state_impl(mut self) -> Self::Next { self.protocol_txs.shrink_to_fit(); - let remaining_free_space = self.uninitialized_space_in_bytes(); + let remaining_free_space = self.unoccupied_space_in_bytes(); self.normal_txs.space = TxBin::init(remaining_free_space); // cast state let BlockAllocator { diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index c4c4de88de..85d2c81474 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -206,7 +206,6 @@ where let ( mut tx_event, - embedding_wrapper, mut tx_gas_meter, mut wrapper_args, ) = match &tx_header.tx_type { @@ -224,7 +223,6 @@ where } ( tx_event, - None, gas_meter, Some(WrapperArgs { block_proposer: &native_block_proposer_address, @@ -246,7 +244,6 @@ where | ProtocolTxType::ValSetUpdateVext | ProtocolTxType::ValidatorSetUpdate => ( new_tx_event(&tx, height.0), - None, TxGasMeter::new_from_sub_limit(0.into()), None, ), @@ -270,7 +267,6 @@ where } ( new_tx_event(&tx, height.0), - None, TxGasMeter::new_from_sub_limit(0.into()), None, ) @@ -297,13 +293,20 @@ where } ( new_tx_event(&tx, height.0), - None, TxGasMeter::new_from_sub_limit(0.into()), None, ) } }, }; + let replay_protection_hashes = if matches!(tx_header.tx_type, TxType::Wrapper(_)) { + Some(ReplayProtectionHashes { + raw_header_hash: tx.raw_header_hash(), + header_hash: tx.header_hash(), + }) + } else { + None + }; let tx_gas_meter = RefCell::new(tx_gas_meter); let tx_result = protocol::check_tx_allowed(&tx, &self.state) .and_then(|()| { @@ -328,8 +331,8 @@ where Ok(result) => { if result.is_accepted() { if wrapper_args - .expect("Missing required wrapper arguments") - .is_committed_fee_unshield + .map(|args| args.is_committed_fee_unshield) + .unwrap_or_default() || result.vps_result.accepted_vps.contains( &Address::Internal( address::InternalAddress::Masp, @@ -352,9 +355,8 @@ where result.wrapper_changed_keys.iter().cloned(), ); stats.increment_successful_txs(); - if let Some(wrapper) = embedding_wrapper { - self.commit_inner_tx_hash(wrapper); - } + self.commit_inner_tx_hash(replay_protection_hashes); + self.state.commit_tx(); if !tx_event.contains_key("code") { tx_event["code"] = ResultCode::Ok.into(); @@ -386,6 +388,7 @@ where ), ); } else { + // this branch can only be reached by inner txs tracing::trace!( "some VPs rejected transaction {} storage \ modification {:#?}", @@ -393,13 +396,11 @@ where result.vps_result.rejected_vps ); - if let Some(wrapper) = embedding_wrapper { - // If decrypted tx failed for any reason but invalid - // signature, commit its hash to storage, otherwise - // allow for a replay - if !result.vps_result.invalid_sig { - self.commit_inner_tx_hash(wrapper); - } + // If an inner tx failed for any reason but invalid + // signature, commit its hash to storage, otherwise + // allow for a replay + if !result.vps_result.invalid_sig { + self.commit_inner_tx_hash(replay_protection_hashes); } stats.increment_rejected_txs(); @@ -410,6 +411,16 @@ where tx_event["info"] = "Check inner_tx for result.".to_string(); tx_event["inner_tx"] = result.to_string(); } + Err(Error::TxApply(protocol::Error::WrapperRunnerError(msg))) => { + tracing::info!( + "Wrapper transaction {} failed with: {}", + tx_event["hash"], + msg, + ); + tx_event["gas_used"] = tx_gas_meter.get_tx_consumed_gas().to_string(); + tx_event["info"] = msg.to_string(); + tx_event["code"] = ResultCode::InvalidTx.into(); + } Err(msg) => { tracing::info!( "Transaction {} failed with: {}", @@ -417,21 +428,21 @@ where msg ); - // If transaction type is Decrypted and didn't fail + // If user transaction didn't fail // because of out of gas nor invalid // section commitment, commit its hash to prevent replays - if let Some(wrapper) = embedding_wrapper { + if matches!(tx_header.tx_type, TxType::Wrapper(_)) { if !matches!( msg, Error::TxApply(protocol::Error::GasError(_)) - | Error::TxApply( - protocol::Error::MissingSection(_) - ) - | Error::TxApply( - protocol::Error::ReplayAttempt(_) - ) + | Error::TxApply( + protocol::Error::MissingSection(_) + ) + | Error::TxApply( + protocol::Error::ReplayAttempt(_) + ) ) { - self.commit_inner_tx_hash(wrapper); + self.commit_inner_tx_hash(replay_protection_hashes); } else if let Error::TxApply( protocol::Error::ReplayAttempt(_), ) = msg @@ -440,8 +451,11 @@ where // hash. A replay of the wrapper is impossible since // the inner tx hash is committed to storage and // we validate the wrapper against that hash too + let header_hash = replay_protection_hashes + .expect("This cannot fail") + .header_hash; self.state - .delete_tx_hash(wrapper.header_hash()) + .delete_tx_hash(header_hash) .expect( "Error while deleting tx hash from storage", ); @@ -598,17 +612,24 @@ where // hash since it's redundant (we check the inner tx hash too when validating // the wrapper). Requires the wrapper transaction as argument to recover // both the hashes. - fn commit_inner_tx_hash(&mut self, wrapper_tx: Tx) { - self.state - .write_tx_hash(wrapper_tx.raw_header_hash()) - .expect("Error while writing tx hash to storage"); - - self.state - .delete_tx_hash(wrapper_tx.header_hash()) - .expect("Error while deleting tx hash from storage"); + fn commit_inner_tx_hash(&mut self, hashes: Option) { + if let Some(ReplayProtectionHashes {raw_header_hash, header_hash}) = hashes { + self.state + .write_tx_hash(raw_header_hash) + .expect("Error while writing tx hash to storage"); + + self.state + .delete_tx_hash(header_hash) + .expect("Error while deleting tx hash from storage"); + } } } +struct ReplayProtectionHashes { + raw_header_hash: Hash, + header_hash: Hash, +} + /// Convert ABCI vote info to PoS vote info. Any info which fails the conversion /// will be skipped and errors logged. /// @@ -745,6 +766,7 @@ mod test_finalize_block { shell: &TestShell, keypair: &common::SecretKey, ) -> (Tx, ProcessedTx) { + let tx_code = TestWasms::TxNoOp.read_bytes(); let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { @@ -757,10 +779,10 @@ mod test_finalize_block { None, )))); wrapper_tx.header.chain_id = shell.chain_id.clone(); - wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned(), None)); wrapper_tx.set_data(Data::new( "Encrypted transaction data".as_bytes().to_owned(), )); + wrapper_tx.set_code(Code::new(tx_code, None)); wrapper_tx.add_section(Section::Signature(Signature::new( wrapper_tx.sechashes(), [(0, keypair.clone())].into_iter().collect(), @@ -787,7 +809,6 @@ mod test_finalize_block { let (mut shell, _, _, _) = setup(); let keypair = gen_keypair(); let mut processed_txs = vec![]; - let mut valid_wrappers = vec![]; // Add unshielded balance for fee payment let balance_key = token::storage_key::balance_key( @@ -800,12 +821,9 @@ mod test_finalize_block { .unwrap(); // create some wrapper txs - for i in 1u64..5 { - let (wrapper, mut processed_tx) = mk_wrapper_tx(&shell, &keypair); + for i in 0u64..4 { + let (_, mut processed_tx) = mk_wrapper_tx(&shell, &keypair); processed_tx.result.code = u32::try_from(i.rem_euclid(2)).unwrap(); - if processed_tx.result.code != 0 { - valid_wrappers.push(wrapper); - } processed_txs.push(processed_tx); } @@ -819,7 +837,7 @@ mod test_finalize_block { .iter() .enumerate() { - assert_eq!(event.event_type.to_string(), String::from("accepted")); + assert_eq!(event.event_type.to_string(), String::from("applied")); let code = event.attributes.get("code").expect("Test failed"); assert_eq!(code, &index.rem_euclid(2).to_string()); } @@ -2383,14 +2401,11 @@ mod test_finalize_block { ..Default::default() }) .expect("Test failed")[0]; - assert_eq!(event.event_type.to_string(), String::from("accepted")); + assert_eq!(event.event_type.to_string(), String::from("applied")); let code = event .attributes .get("code") - .expect( - "Test - failed", - ) + .expect("Test failed") .as_str(); assert_eq!(code, String::from(ResultCode::Ok).as_str()); @@ -2404,7 +2419,7 @@ mod test_finalize_block { .shell .state .write_log() - .has_replay_protection_entry(&wrapper_tx.header_hash()) + .has_replay_protection_entry(&wrapper_tx.raw_header_hash()) .unwrap_or_default() ); // Check that the hash is present in the merkle tree @@ -2423,11 +2438,10 @@ mod test_finalize_block { /// Test that a tx that has already been applied in the same block /// doesn't get reapplied #[test] - fn test_duplicated_decrypted_tx_same_block() { + fn test_duplicated_tx_same_block() { let (mut shell, _, _, _) = setup(); - let keypair = gen_keypair(); - let keypair_2 = gen_keypair(); - let mut batch = namada::state::testing::TestState::batch(); + let keypair = crate::wallet::defaults::albert_keypair(); + let keypair_2 = crate::wallet::defaults::bertha_keypair(); let tx_code = TestWasms::TxNoOp.read_bytes(); let mut wrapper = @@ -2467,22 +2481,10 @@ mod test_finalize_block { None, ))); - let inner = wrapper.clone(); - let new_inner = new_wrapper.clone(); - - // Write wrapper hashes in storage - for tx in [&wrapper, &new_wrapper] { - let hash_subkey = replay_protection::last_key(&tx.header_hash()); - shell - .state - .write_replay_protection_entry(&mut batch, &hash_subkey) - .expect("Test failed"); - } - let mut processed_txs: Vec = vec![]; - for inner in [&inner, &new_inner] { + for tx in [&wrapper, &new_wrapper] { processed_txs.push(ProcessedTx { - tx: inner.to_bytes().into(), + tx: tx.to_bytes().into(), result: TxResult { code: ResultCode::Ok.into(), info: "".into(), @@ -2511,12 +2513,12 @@ mod test_finalize_block { let code = event[1].attributes.get("code").unwrap().as_str(); assert_eq!(code, String::from(ResultCode::WasmRuntimeError).as_str()); - for (inner, wrapper) in [(inner, wrapper), (new_inner, new_wrapper)] { + for wrapper in [&wrapper, &new_wrapper] { assert!( shell .state .write_log() - .has_replay_protection_entry(&inner.raw_header_hash()) + .has_replay_protection_entry(&wrapper.raw_header_hash()) .unwrap_or_default() ); assert!( @@ -2536,15 +2538,39 @@ mod test_finalize_block { #[test] fn test_tx_hash_handling() { let (mut shell, _, _, _) = setup(); - let keypair = gen_keypair(); - let mut batch = namada::state::testing::TestState::batch(); + let keypair = crate::wallet::defaults::bertha_keypair(); + let mut out_of_gas_wrapper = { + let tx_code = TestWasms::TxNoOp.read_bytes(); + let mut wrapper_tx = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: DenominatedAmount::native(1.into()), + token: shell.state.in_mem().native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + 0.into(), + None, + )))); + wrapper_tx.header.chain_id = shell.chain_id.clone(); + wrapper_tx.set_data(Data::new( + "Encrypted transaction data".as_bytes().to_owned(), + )); + wrapper_tx.set_code(Code::new(tx_code, None)); + wrapper_tx.add_section(Section::Signature(Signature::new( + wrapper_tx.sechashes(), + [(0, keypair.clone())].into_iter().collect(), + None, + ))); + wrapper_tx + }; - let (out_of_gas_wrapper, _) = mk_wrapper_tx(&shell, &keypair); let mut wasm_path = top_level_directory(); // Write a key to trigger the vp to validate the signature wasm_path.push("wasm_for_tests/tx_write.wasm"); let tx_code = std::fs::read(wasm_path) .expect("Expected a file at given code path"); + let mut unsigned_wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { @@ -2559,7 +2585,9 @@ mod test_finalize_block { None, )))); unsigned_wrapper.header.chain_id = shell.chain_id.clone(); + let mut failing_wrapper = unsigned_wrapper.clone(); + unsigned_wrapper.set_code(Code::new(tx_code, None)); let addr = Address::from(&keypair.to_public()); let key = Key::from(addr.to_db_key()) @@ -2571,6 +2599,7 @@ mod test_finalize_block { }) .unwrap(), )); + let mut wasm_path = top_level_directory(); wasm_path.push("wasm_for_tests/tx_fail.wasm"); let tx_code = std::fs::read(wasm_path) @@ -2579,41 +2608,32 @@ mod test_finalize_block { failing_wrapper.set_data(Data::new( "Encrypted transaction data".as_bytes().to_owned(), )); - let mut wrong_commitment_wrapper = failing_wrapper.clone(); - wrong_commitment_wrapper.set_code_sechash(Hash::default()); - let out_of_gas_inner = out_of_gas_wrapper.clone(); - let unsigned_inner = unsigned_wrapper.clone(); - let mut wrong_commitment_inner = failing_wrapper.clone(); + let mut wrong_commitment_wrapper = failing_wrapper.clone(); + let tx_code = TestWasms::TxInvalidData.read_bytes(); + wrong_commitment_wrapper.set_code(Code::new(tx_code, None)); + wrong_commitment_wrapper.sections.retain(|sec| !matches!(sec, Section::Data(_))); // Add some extra data to avoid having the same Tx hash as the // `failing_wrapper` - wrong_commitment_inner.add_memo(&[0_u8]); - let failing_inner = failing_wrapper.clone(); - - // Write wrapper hashes in storage - for wrapper in [ - &out_of_gas_wrapper, - &unsigned_wrapper, - &wrong_commitment_wrapper, - &failing_wrapper, - ] { - let hash_subkey = - replay_protection::last_key(&wrapper.header_hash()); - shell - .state - .write_replay_protection_entry(&mut batch, &hash_subkey) - .unwrap(); - } + wrong_commitment_wrapper.add_memo(&[0_u8]); let mut processed_txs: Vec = vec![]; - for inner in [ - &out_of_gas_inner, - &unsigned_inner, - &wrong_commitment_inner, - &failing_inner, + for tx in [ + &mut out_of_gas_wrapper, + &mut wrong_commitment_wrapper, + &mut failing_wrapper, ] { + tx.sign_raw(vec![keypair.clone()], vec![keypair.ref_to()].into_iter().collect(), None); + } + for tx in [ + &mut out_of_gas_wrapper, + &mut unsigned_wrapper, + &mut wrong_commitment_wrapper, + &mut failing_wrapper, + ] { + tx.sign_wrapper(keypair.clone()); processed_txs.push(ProcessedTx { - tx: inner.to_bytes().into(), + tx: tx.to_bytes().into(), result: TxResult { code: ResultCode::Ok.into(), info: "".into(), @@ -2637,31 +2657,28 @@ mod test_finalize_block { assert_eq!(event[0].event_type.to_string(), String::from("applied")); let code = event[0].attributes.get("code").unwrap().as_str(); - assert_eq!(code, String::from(ResultCode::WasmRuntimeError).as_str()); + assert_eq!(code, String::from(ResultCode::InvalidTx).as_str()); assert_eq!(event[1].event_type.to_string(), String::from("applied")); let code = event[1].attributes.get("code").unwrap().as_str(); - assert_eq!(code, String::from(ResultCode::Undecryptable).as_str()); + assert_eq!(code, String::from(ResultCode::InvalidTx).as_str()); assert_eq!(event[2].event_type.to_string(), String::from("applied")); let code = event[2].attributes.get("code").unwrap().as_str(); - assert_eq!(code, String::from(ResultCode::InvalidTx).as_str()); + assert_eq!(code, String::from(ResultCode::WasmRuntimeError).as_str()); assert_eq!(event[3].event_type.to_string(), String::from("applied")); let code = event[3].attributes.get("code").unwrap().as_str(); assert_eq!(code, String::from(ResultCode::WasmRuntimeError).as_str()); - assert_eq!(event[4].event_type.to_string(), String::from("applied")); - let code = event[4].attributes.get("code").unwrap().as_str(); - assert_eq!(code, String::from(ResultCode::WasmRuntimeError).as_str()); - for (invalid_inner, valid_wrapper) in [ - (out_of_gas_inner, out_of_gas_wrapper), - (unsigned_inner, unsigned_wrapper), - (wrong_commitment_inner, wrong_commitment_wrapper), + for valid_wrapper in [ + out_of_gas_wrapper, + unsigned_wrapper, + wrong_commitment_wrapper, ] { assert!( !shell .state .write_log() .has_replay_protection_entry( - &invalid_inner.raw_header_hash() + &valid_wrapper.raw_header_hash() ) .unwrap_or_default() ); @@ -2676,7 +2693,7 @@ mod test_finalize_block { shell .state .write_log() - .has_replay_protection_entry(&failing_inner.raw_header_hash()) + .has_replay_protection_entry(&failing_wrapper.raw_header_hash()) .expect("test failed") ); assert!( @@ -2743,7 +2760,7 @@ mod test_finalize_block { let root_post = shell.shell.state.in_mem().block.tree.root(); assert_eq!(root_pre.0, root_post.0); - assert_eq!(event[0].event_type.to_string(), String::from("accepted")); + assert_eq!(event[0].event_type.to_string(), String::from("applied")); let code = event[0] .attributes .get("code") @@ -2813,8 +2830,8 @@ mod test_finalize_block { .expect("Test failed")[0]; // Check balance of fee payer is 0 - assert_eq!(event.event_type.to_string(), String::from("accepted")); - let code = event.attributes.get("code").expect("Testfailed").as_str(); + assert_eq!(event.event_type.to_string(), String::from("applied")); + let code = event.attributes.get("code").expect("Test failed").as_str(); assert_eq!(code, String::from(ResultCode::InvalidTx).as_str()); let balance_key = token::storage_key::balance_key( &shell.state.in_mem().native_token, @@ -2912,7 +2929,7 @@ mod test_finalize_block { .expect("Test failed")[0]; // Check fee payment - assert_eq!(event.event_type.to_string(), String::from("accepted")); + assert_eq!(event.event_type.to_string(), String::from("applied")); let code = event.attributes.get("code").expect("Test failed").as_str(); assert_eq!(code, String::from(ResultCode::Ok).as_str()); diff --git a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs index be84ef0af2..aec14cf18b 100644 --- a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -410,13 +410,13 @@ where // incentivize the proposer to include only // valid transaction and avoid wasting block // resources (ABCI only) + + // Account for the tx's resources even in case of an error. + let allocated_gas = metadata + .user_gas + .try_dump(u64::from(wrapper.gas_limit)); let mut tx_gas_meter = TxGasMeter::new(wrapper.gas_limit); - if tx_gas_meter.add_wrapper_gas(tx_bytes).is_err() { - // Account for the tx's resources even in case of an error. - // Ignore any allocation error - let _ = metadata - .user_gas - .try_dump(u64::from(wrapper.gas_limit)); + if tx_gas_meter.add_wrapper_gas(tx_bytes).is_err() || allocated_gas.is_err() { return TxResult { code: ResultCode::TxGasLimit.into(), @@ -1435,7 +1435,7 @@ mod test_process_proposal { Err(TestError::RejectProposal(response)) => { assert_eq!( response[0].result.code, - u32::from(ResultCode::AllocationError) + u32::from(ResultCode::TxGasLimit) ); } } diff --git a/crates/apps/src/lib/node/ledger/storage/rocksdb.rs b/crates/apps/src/lib/node/ledger/storage/rocksdb.rs index a329589b5e..989c152d46 100644 --- a/crates/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/crates/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -7,14 +7,12 @@ //! - `eth_events_queue`: a queue of confirmed ethereum events to be processed //! in order //! - `height`: the last committed block height -//! - `tx_queue`: txs to be decrypted in the next block //! - `next_epoch_min_start_height`: minimum block height from which the next //! epoch can start //! - `next_epoch_min_start_time`: minimum block time from which the next //! epoch can start //! - `replay_protection`: hashes of the processed transactions //! - `pred`: predecessor values of the top-level keys of the same name -//! - `tx_queue` //! - `next_epoch_min_start_height` //! - `next_epoch_min_start_time` //! - `conversion_state`: MASP conversion state @@ -515,7 +513,6 @@ impl RocksDB { for metadata_key in [ "next_epoch_min_start_height", "next_epoch_min_start_time", - "tx_queue", ] { let previous_key = format!("pred/{}", metadata_key); let previous_value = self @@ -989,15 +986,6 @@ impl DB for RocksDB { ); } - // Tx queue - if let Some(pred_tx_queue) = self - .0 - .get_cf(state_cf, "tx_queue") - .map_err(|e| Error::DBError(e.into_string()))? - { - // Write the predecessor value for rollback - batch.0.put_cf(state_cf, "pred/tx_queue", pred_tx_queue); - } batch .0 .put_cf(state_cf, "ethereum_height", encode(ðereum_height)); diff --git a/crates/namada/src/ledger/protocol/mod.rs b/crates/namada/src/ledger/protocol/mod.rs index 7641fc77d0..d7202998c3 100644 --- a/crates/namada/src/ledger/protocol/mod.rs +++ b/crates/namada/src/ledger/protocol/mod.rs @@ -48,6 +48,8 @@ pub enum Error { StateError(namada_state::Error), #[error("Storage error: {0}")] StorageError(namada_state::StorageError), + #[error("Wrapper tx runner error: {0}")] + WrapperRunnerError(String), #[error("Transaction runner error: {0}")] TxRunnerError(vm::wasm::run::Error), #[error("{0:?}")] @@ -200,7 +202,7 @@ where tx_wasm_cache, }, wrapper_args, - )?; + ).map_err(|e| Error::WrapperRunnerError(e.to_string()))?; let mut inner_res = apply_wasm_tx( tx, &tx_index, @@ -211,6 +213,7 @@ where tx_wasm_cache, }, )?; + inner_res.wrapper_changed_keys = changed_keys; Ok(inner_res) } @@ -1192,7 +1195,22 @@ mod tests { fn test_apply_wasm_tx_allowlist() { let (mut state, _validators) = test_utils::setup_default_storage(); - let mut tx = Tx::new(ChainId::default(), None); + let mut rng: ThreadRng = thread_rng(); + ed25519::SigScheme::generate(&mut rng).try_to_sk().unwrap() + }; + let wrapper_tx = WrapperTx::new( + Fee { + amount_per_gas_unit: DenominatedAmount::native( + Amount::from_uint(10, 0).expect("Test failed"), + ), + token: nam(), + }, + keypair.ref_to(), + Epoch(0), + Default::default(), + None, + ); + let mut tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper_tx))); // pseudo-random code hash let code = vec![1_u8, 2, 3]; let tx_hash = Hash::sha256(&code); diff --git a/crates/state/src/wl_storage.rs b/crates/state/src/wl_storage.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/state/src/write_log.rs b/crates/state/src/write_log.rs index 6ce1f31d54..ec2597d56c 100644 --- a/crates/state/src/write_log.rs +++ b/crates/state/src/write_log.rs @@ -618,12 +618,12 @@ impl WriteLog { Some(_) => // Cannot delete an hash that still has to be written to // storage or has already been deleted - { - Err(Error::ReplayProtection(format!( - "Requested a delete on hash {hash} not yet committed to \ + { + Err(Error::ReplayProtection(format!( + "Requested a delete on hash {hash} not yet committed to \ storage" - ))) - } + ))) + } } } diff --git a/crates/test_utils/src/lib.rs b/crates/test_utils/src/lib.rs index b0872c600f..a391ea70e3 100644 --- a/crates/test_utils/src/lib.rs +++ b/crates/test_utils/src/lib.rs @@ -19,6 +19,7 @@ pub const WASM_FOR_TESTS_DIR: &str = "wasm_for_tests"; pub enum TestWasms { TxMemoryLimit, TxNoOp, + TxInvalidData, TxProposalCode, TxReadStorageKey, TxWriteStorageKey, @@ -37,6 +38,7 @@ impl TestWasms { let filename = match self { TestWasms::TxMemoryLimit => "tx_memory_limit.wasm", TestWasms::TxNoOp => "tx_no_op.wasm", + TestWasms::TxInvalidData => "tx_invalid_data.wasm", TestWasms::TxProposalCode => "tx_proposal_code.wasm", TestWasms::TxReadStorageKey => "tx_read_storage_key.wasm", TestWasms::TxWriteStorageKey => "tx_write.wasm", diff --git a/wasm_for_tests/tx_invalid_data.wasm b/wasm_for_tests/tx_invalid_data.wasm new file mode 100755 index 0000000000000000000000000000000000000000..7158c51405360314eb021d69c100e535e9a50f30 GIT binary patch literal 423715 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

7FLLE*q`WhSkwW*Ix{_;D&@<%QRqmBlQe&kQS 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 e&hYyF<|F42Ibm4o2~_ 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

G)S(&kfic2tG_uwqG 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 literal 0 HcmV?d00001 diff --git a/wasm_for_tests/wasm_source/Cargo.toml b/wasm_for_tests/wasm_source/Cargo.toml index 257d8db8f6..c7f0ddcca8 100644 --- a/wasm_for_tests/wasm_source/Cargo.toml +++ b/wasm_for_tests/wasm_source/Cargo.toml @@ -22,6 +22,7 @@ vp_always_true = [] vp_eval = [] vp_memory_limit = [] vp_read_storage_key = [] +tx_invalid_data = [] tx_proposal_code = [] tx_proposal_masp_reward = [] diff --git a/wasm_for_tests/wasm_source/Makefile b/wasm_for_tests/wasm_source/Makefile index c8783dc001..688a39668b 100644 --- a/wasm_for_tests/wasm_source/Makefile +++ b/wasm_for_tests/wasm_source/Makefile @@ -15,6 +15,7 @@ wasms += vp_always_true wasms += vp_eval wasms += vp_memory_limit wasms += vp_read_storage_key +wasms += tx_invalid_data wasms += tx_proposal_code wasms += tx_proposal_masp_reward diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index 1345a5634c..460bdffcfe 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -158,6 +158,21 @@ pub mod main { } } +#[cfg(feature = "tx_invalid_data")] +pub mod main { + use namada_tx_prelude::*; + + #[transaction(gas = 1000)] + fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { + let signed = tx_data; + let _data = signed.data().ok_or_err_msg("Missing data").map_err(|err| { + ctx.set_commitment_sentinel(); + err + })?; + Ok(()) + } +} + /// A VP that always returns `true`. #[cfg(feature = "vp_always_true")] pub mod main { From 57382885c00242bc21547723fb01b278e61fd44a Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 15 Feb 2024 15:16:25 +0100 Subject: [PATCH 03/17] fixed integration tests --- .../src/lib/node/ledger/shell/testing/node.rs | 20 ++----------------- crates/sdk/src/masp.rs | 12 +++++------ 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/crates/apps/src/lib/node/ledger/shell/testing/node.rs b/crates/apps/src/lib/node/ledger/shell/testing/node.rs index 93d7778aaf..11fb4d73f7 100644 --- a/crates/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/crates/apps/src/lib/node/ledger/shell/testing/node.rs @@ -503,9 +503,8 @@ impl MockNode { /// Send a tx through Process Proposal and Finalize Block /// and register the results. - pub fn submit_txs(&self, txs: Vec>) { - // The block space allocator disallows encrypted txs in certain blocks. - // Advance to block height that allows txs. + fn submit_txs(&self, txs: Vec>) { + self.finalize_and_commit(); let (proposer_address, votes) = self.prepare_request(); @@ -765,21 +764,6 @@ impl<'a> Client for &'a MockNode { } else { self.clear_results(); } - let (proposer_address, _) = self.prepare_request(); - let req = RequestPrepareProposal { - proposer_address: proposer_address.into(), - ..Default::default() - }; - let txs: Vec> = { - let locked = self.shell.lock().unwrap(); - locked.prepare_proposal(req).txs - } - .into_iter() - .map(|tx| tx.into()) - .collect(); - if !txs.is_empty() { - self.submit_txs(txs); - } Ok(resp) } diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index 22ec3fa58c..074a4676dd 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -895,11 +895,10 @@ impl ShieldedContext { ) -> Result { // We use the changed keys instead of the Transfer object // because those are what the masp validity predicate works on - let TxResult { + let ( wrapper_changed_keys, changed_keys, - .. - } = if let ExtractShieldedActionArg::Event(tx_event) = action_arg { + ) = if let ExtractShieldedActionArg::Event(tx_event) = action_arg { let tx_result_str = tx_event .attributes .iter() @@ -915,10 +914,11 @@ impl ShieldedContext { "Missing required tx result in event".to_string(), ) })?; - TxResult::from_str(tx_result_str) - .map_err(|e| Error::Other(e.to_string()))? + let result = TxResult::from_str(tx_result_str) + .map_err(|e| Error::Other(e.to_string()))?; + (result.wrapper_changed_keys, result.changed_keys) } else { - panic!("Expected a event type Shield action argument.") + (Default::default(), Default::default()) }; let tx_header = tx.header(); From 3f06283028d6c240c4ee319fc2df02708434c028 Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 16 Feb 2024 12:53:12 +0100 Subject: [PATCH 04/17] Fixing e2e tests --- .../src/lib/node/ledger/shell/block_alloc.rs | 7 +- .../node/ledger/shell/block_alloc/states.rs | 12 +- .../lib/node/ledger/shell/finalize_block.rs | 124 +++++++++--------- .../lib/node/ledger/shell/process_proposal.rs | 10 +- .../src/lib/node/ledger/shell/testing/node.rs | 1 - crates/namada/src/ledger/protocol/mod.rs | 3 +- crates/sdk/src/masp.rs | 48 ++++--- crates/tests/src/e2e/eth_bridge_tests.rs | 1 - crates/tests/src/e2e/helpers.rs | 3 +- crates/tests/src/e2e/ibc_tests.rs | 9 +- crates/tests/src/e2e/ledger_tests.rs | 21 ++- crates/tests/src/strings.rs | 3 - 12 files changed, 119 insertions(+), 123 deletions(-) diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index 5ce22be212..1d387bd7d6 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -180,7 +180,7 @@ impl BlockAllocator { _state: PhantomData, block: TxBin::init(max), protocol_txs: TxBin::default(), - normal_txs: NormalTxsBins{ + normal_txs: NormalTxsBins { space: TxBin::init(tendermint_max_block_space_in_bytes), gas: TxBin::init(max_block_gas), }, @@ -496,7 +496,10 @@ mod tests { 1_000, ); let expected = tendermint_max_block_space_in_bytes; - assert_eq!(bins.protocol_txs.allotted, threshold::ONE_HALF.over(tendermint_max_block_space_in_bytes)); + assert_eq!( + bins.protocol_txs.allotted, + threshold::ONE_HALF.over(tendermint_max_block_space_in_bytes) + ); assert_eq!(expected, bins.unoccupied_space_in_bytes()); } diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs index 94ba14f574..64587d5464 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs @@ -1,4 +1,4 @@ -//! All the states of the [`BlockAllocator`] state machine, +//! All the states of the `BlockAllocator` state machine, //! over the extent of a Tendermint consensus round //! block proposal. //! @@ -51,7 +51,7 @@ pub enum WithoutNormalTxs {} /// [`crate::node::ledger::shell::block_alloc::states`]. pub struct BuildingTxBatch {} -/// Try to allocate a new transaction on a [`BlockAllocator`] state. +/// Try to allocate a new transaction on a `BlockAllocator` state. /// /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. @@ -65,7 +65,7 @@ pub trait TryAlloc { ) -> Result<(), AllocFailure>; } -/// Represents a state transition in the [`BlockAllocator`] state machine. +/// Represents a state transition in the `BlockAllocator` state machine. /// /// This trait should not be used directly. Instead, consider using /// [`NextState`]. @@ -73,10 +73,10 @@ pub trait TryAlloc { /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. pub trait NextStateImpl { - /// The next state in the [`BlockAllocator`] state machine. + /// The next state in the `BlockAllocator` state machine. type Next; - /// Transition to the next state in the [`BlockAllocator`] state + /// Transition to the next state in the `BlockAllocator`] state /// machine. fn next_state_impl(self) -> Self::Next; } @@ -87,7 +87,7 @@ pub trait NextStateImpl { /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. pub trait NextState: NextStateImpl { - /// Transition to the next state in the [`BlockAllocator`] state, + /// Transition to the next state in the `BlockAllocator` state, /// using a null transiiton function. #[inline] fn next_state(self) -> Self::Next diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index 85d2c81474..17825b18bf 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -207,48 +207,48 @@ where let ( mut tx_event, mut tx_gas_meter, - mut wrapper_args, + mut wrapper_args ) = match &tx_header.tx_type { - TxType::Wrapper(wrapper) => { - stats.increment_wrapper_txs(); - let tx_event = new_tx_event(&tx, height.0); - let gas_meter = TxGasMeter::new(wrapper.gas_limit); - if let Some(code_sec) = tx - .get_section(tx.code_sechash()) - .and_then(|x| Section::code_sec(x.as_ref())) - { - stats.increment_tx_type( - code_sec.code.hash().to_string(), + TxType::Wrapper(wrapper) => { + stats.increment_wrapper_txs(); + let tx_event = new_tx_event(&tx, height.0); + let gas_meter = TxGasMeter::new(wrapper.gas_limit); + if let Some(code_sec) = tx + .get_section(tx.code_sechash()) + .and_then(|x| Section::code_sec(x.as_ref())) + { + stats.increment_tx_type( + code_sec.code.hash().to_string(), + ); + } + ( + tx_event, + gas_meter, + Some(WrapperArgs { + block_proposer: &native_block_proposer_address, + is_committed_fee_unshield: false, + }), + ) + } + TxType::Decrypted(_) => unreachable!(), + TxType::Raw => { + tracing::error!( + "Internal logic error: FinalizeBlock received a \ + TxType::Raw transaction" ); + continue; } - ( - tx_event, - gas_meter, - Some(WrapperArgs { - block_proposer: &native_block_proposer_address, - is_committed_fee_unshield: false, - }), - ) - } - TxType::Decrypted(_) => unreachable!(), - TxType::Raw => { - tracing::error!( - "Internal logic error: FinalizeBlock received a \ - TxType::Raw transaction" - ); - continue; - } - TxType::Protocol(protocol_tx) => match protocol_tx.tx { - ProtocolTxType::BridgePoolVext - | ProtocolTxType::BridgePool - | ProtocolTxType::ValSetUpdateVext - | ProtocolTxType::ValidatorSetUpdate => ( - new_tx_event(&tx, height.0), - TxGasMeter::new_from_sub_limit(0.into()), - None, - ), - ProtocolTxType::EthEventsVext => { - let ext = + TxType::Protocol(protocol_tx) => match protocol_tx.tx { + ProtocolTxType::BridgePoolVext + | ProtocolTxType::BridgePool + | ProtocolTxType::ValSetUpdateVext + | ProtocolTxType::ValidatorSetUpdate => ( + new_tx_event(&tx, height.0), + TxGasMeter::new_from_sub_limit(0.into()), + None, + ), + ProtocolTxType::EthEventsVext => { + let ext = ethereum_tx_data_variants::EthEventsVext::try_from( &tx, ) @@ -411,13 +411,16 @@ where tx_event["info"] = "Check inner_tx for result.".to_string(); tx_event["inner_tx"] = result.to_string(); } - Err(Error::TxApply(protocol::Error::WrapperRunnerError(msg))) => { + Err(Error::TxApply(protocol::Error::WrapperRunnerError( + msg, + ))) => { tracing::info!( "Wrapper transaction {} failed with: {}", tx_event["hash"], msg, ); - tx_event["gas_used"] = tx_gas_meter.get_tx_consumed_gas().to_string(); + tx_event["gas_used"] = + tx_gas_meter.get_tx_consumed_gas().to_string(); tx_event["info"] = msg.to_string(); tx_event["code"] = ResultCode::InvalidTx.into(); } @@ -435,12 +438,12 @@ where if !matches!( msg, Error::TxApply(protocol::Error::GasError(_)) - | Error::TxApply( - protocol::Error::MissingSection(_) - ) - | Error::TxApply( - protocol::Error::ReplayAttempt(_) - ) + | Error::TxApply( + protocol::Error::MissingSection(_) + ) + | Error::TxApply( + protocol::Error::ReplayAttempt(_) + ) ) { self.commit_inner_tx_hash(replay_protection_hashes); } else if let Error::TxApply( @@ -473,9 +476,10 @@ where tx_event["code"] = ResultCode::InvalidTx.into(); // The fee unshield operation could still have been // committed - if wrapper_args - .expect("Missing required wrapper arguments") - .is_committed_fee_unshield + if let Some(WrapperArgs { + is_committed_fee_unshield: true, + .. + }) = wrapper_args { tx_event["is_valid_masp_tx"] = format!("{}", tx_index); } @@ -2402,11 +2406,7 @@ mod test_finalize_block { }) .expect("Test failed")[0]; assert_eq!(event.event_type.to_string(), String::from("applied")); - let code = event - .attributes - .get("code") - .expect("Test failed") - .as_str(); + let code = event.attributes.get("code").expect("Test failed").as_str(); assert_eq!(code, String::from(ResultCode::Ok).as_str()); // the merkle tree root should not change after finalize_block @@ -2441,7 +2441,7 @@ mod test_finalize_block { fn test_duplicated_tx_same_block() { let (mut shell, _, _, _) = setup(); let keypair = crate::wallet::defaults::albert_keypair(); - let keypair_2 = crate::wallet::defaults::bertha_keypair(); + let keypair_2 = crate::wallet::defaults::bertha_keypair(); let tx_code = TestWasms::TxNoOp.read_bytes(); let mut wrapper = @@ -2545,7 +2545,7 @@ mod test_finalize_block { Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { amount_per_gas_unit: DenominatedAmount::native(1.into()), - token: shell.state.in_mem().native_token.clone(), + token: shell.state.native_token.clone(), }, keypair.ref_to(), Epoch(0), @@ -2612,7 +2612,9 @@ mod test_finalize_block { let mut wrong_commitment_wrapper = failing_wrapper.clone(); let tx_code = TestWasms::TxInvalidData.read_bytes(); wrong_commitment_wrapper.set_code(Code::new(tx_code, None)); - wrong_commitment_wrapper.sections.retain(|sec| !matches!(sec, Section::Data(_))); + wrong_commitment_wrapper + .sections + .retain(|sec| !matches!(sec, Section::Data(_))); // Add some extra data to avoid having the same Tx hash as the // `failing_wrapper` wrong_commitment_wrapper.add_memo(&[0_u8]); @@ -2623,7 +2625,11 @@ mod test_finalize_block { &mut wrong_commitment_wrapper, &mut failing_wrapper, ] { - tx.sign_raw(vec![keypair.clone()], vec![keypair.ref_to()].into_iter().collect(), None); + tx.sign_raw( + vec![keypair.clone()], + vec![keypair.ref_to()].into_iter().collect(), + None, + ); } for tx in [ &mut out_of_gas_wrapper, diff --git a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs index aec14cf18b..adc7de1ac2 100644 --- a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -412,12 +412,12 @@ where // resources (ABCI only) // Account for the tx's resources even in case of an error. - let allocated_gas = metadata - .user_gas - .try_dump(u64::from(wrapper.gas_limit)); + let allocated_gas = + metadata.user_gas.try_dump(u64::from(wrapper.gas_limit)); let mut tx_gas_meter = TxGasMeter::new(wrapper.gas_limit); - if tx_gas_meter.add_wrapper_gas(tx_bytes).is_err() || allocated_gas.is_err() { - + if tx_gas_meter.add_wrapper_gas(tx_bytes).is_err() + || allocated_gas.is_err() + { return TxResult { code: ResultCode::TxGasLimit.into(), info: "Wrapper transactions exceeds its gas limit" diff --git a/crates/apps/src/lib/node/ledger/shell/testing/node.rs b/crates/apps/src/lib/node/ledger/shell/testing/node.rs index 11fb4d73f7..0d75470964 100644 --- a/crates/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/crates/apps/src/lib/node/ledger/shell/testing/node.rs @@ -504,7 +504,6 @@ impl MockNode { /// Send a tx through Process Proposal and Finalize Block /// and register the results. fn submit_txs(&self, txs: Vec>) { - self.finalize_and_commit(); let (proposer_address, votes) = self.prepare_request(); diff --git a/crates/namada/src/ledger/protocol/mod.rs b/crates/namada/src/ledger/protocol/mod.rs index d7202998c3..0507cc26ea 100644 --- a/crates/namada/src/ledger/protocol/mod.rs +++ b/crates/namada/src/ledger/protocol/mod.rs @@ -202,7 +202,8 @@ where tx_wasm_cache, }, wrapper_args, - ).map_err(|e| Error::WrapperRunnerError(e.to_string()))?; + ) + .map_err(|e| Error::WrapperRunnerError(e.to_string()))?; let mut inner_res = apply_wasm_tx( tx, &tx_index, diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index 074a4676dd..c7202ff760 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -895,31 +895,29 @@ impl ShieldedContext { ) -> Result { // We use the changed keys instead of the Transfer object // because those are what the masp validity predicate works on - let ( - wrapper_changed_keys, - changed_keys, - ) = if let ExtractShieldedActionArg::Event(tx_event) = action_arg { - let tx_result_str = tx_event - .attributes - .iter() - .find_map(|attr| { - if attr.key == "inner_tx" { - Some(&attr.value) - } else { - None - } - }) - .ok_or_else(|| { - Error::Other( - "Missing required tx result in event".to_string(), - ) - })?; - let result = TxResult::from_str(tx_result_str) - .map_err(|e| Error::Other(e.to_string()))?; - (result.wrapper_changed_keys, result.changed_keys) - } else { - (Default::default(), Default::default()) - }; + let (wrapper_changed_keys, changed_keys) = + if let ExtractShieldedActionArg::Event(tx_event) = action_arg { + let tx_result_str = tx_event + .attributes + .iter() + .find_map(|attr| { + if attr.key == "inner_tx" { + Some(&attr.value) + } else { + None + } + }) + .ok_or_else(|| { + Error::Other( + "Missing required tx result in event".to_string(), + ) + })?; + let result = TxResult::from_str(tx_result_str) + .map_err(|e| Error::Other(e.to_string()))?; + (result.wrapper_changed_keys, result.changed_keys) + } else { + (Default::default(), Default::default()) + }; let tx_header = tx.header(); // NOTE: simply looking for masp sections attached to the tx diff --git a/crates/tests/src/e2e/eth_bridge_tests.rs b/crates/tests/src/e2e/eth_bridge_tests.rs index 9d7eee8ed8..9c77ab85b3 100644 --- a/crates/tests/src/e2e/eth_bridge_tests.rs +++ b/crates/tests/src/e2e/eth_bridge_tests.rs @@ -826,7 +826,6 @@ async fn test_wdai_transfer_established_unauthorized() -> Result<()> { &bertha_addr.to_string(), &token::DenominatedAmount::new(token::Amount::from(10_000), 0u8.into()), )?; - cmd.exp_string(TX_ACCEPTED)?; cmd.exp_string(TX_REJECTED)?; cmd.assert_success(); diff --git a/crates/tests/src/e2e/helpers.rs b/crates/tests/src/e2e/helpers.rs index 6381bdb753..09761833e7 100644 --- a/crates/tests/src/e2e/helpers.rs +++ b/crates/tests/src/e2e/helpers.rs @@ -36,7 +36,7 @@ use super::setup::{ ENV_VAR_USE_PREBUILT_BINARIES, }; use crate::e2e::setup::{Bin, Who, APPS_PACKAGE}; -use crate::strings::{LEDGER_STARTED, TX_ACCEPTED, TX_APPLIED_SUCCESS}; +use crate::strings::{LEDGER_STARTED, TX_APPLIED_SUCCESS}; use crate::{run, run_as}; /// Instantiate a new [`HttpClient`] to perform RPC requests with. @@ -100,7 +100,6 @@ pub fn init_established_account( rpc_addr, ]; let mut cmd = run!(test, Bin::Client, init_account_args, Some(40))?; - cmd.exp_string(TX_ACCEPTED)?; cmd.exp_string(TX_APPLIED_SUCCESS)?; cmd.assert_success(); Ok(()) diff --git a/crates/tests/src/e2e/ibc_tests.rs b/crates/tests/src/e2e/ibc_tests.rs index c39a4d9bbe..8b2211a45b 100644 --- a/crates/tests/src/e2e/ibc_tests.rs +++ b/crates/tests/src/e2e/ibc_tests.rs @@ -94,7 +94,7 @@ use crate::e2e::setup::{ self, run_hermes_cmd, setup_hermes, sleep, Bin, NamadaCmd, Test, Who, }; use crate::strings::{ - LEDGER_STARTED, TX_ACCEPTED, TX_APPLIED_SUCCESS, TX_FAILED, VALIDATOR_NODE, + LEDGER_STARTED, TX_APPLIED_SUCCESS, TX_FAILED, VALIDATOR_NODE, }; use crate::{run, run_as}; @@ -205,7 +205,7 @@ fn run_ledger_ibc() -> Result<()> { } #[test] -fn run_ledger_ibc_with_hermes() -> Result<()> { +fn drun_ledger_ibc_with_hermes() -> Result<()> { let update_genesis = |mut genesis: templates::All, base_dir: &_| { genesis.parameters.parameters.epochs_per_year = 31536; @@ -1148,7 +1148,6 @@ fn transfer_on_chain( &rpc, ]; let mut client = run!(test, Bin::Client, tx_args, Some(120))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); @@ -1662,7 +1661,6 @@ fn delegate_token(test: &Test) -> Result<()> { &rpc, ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); Ok(()) @@ -1710,7 +1708,6 @@ fn propose_funding( &rpc_a, ]; let mut client = run!(test_a, Bin::Client, submit_proposal_args, Some(40))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); Ok(start_epoch.into()) @@ -1738,7 +1735,6 @@ fn submit_votes(test: &Test) -> Result<()> { submit_proposal_vote, Some(40) )?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); @@ -1756,7 +1752,6 @@ fn submit_votes(test: &Test) -> Result<()> { ]; let mut client = run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); Ok(()) diff --git a/crates/tests/src/e2e/ledger_tests.rs b/crates/tests/src/e2e/ledger_tests.rs index 17c85d5fd7..2d5fc7a8ee 100644 --- a/crates/tests/src/e2e/ledger_tests.rs +++ b/crates/tests/src/e2e/ledger_tests.rs @@ -58,8 +58,8 @@ use crate::e2e::setup::{ Who, }; use crate::strings::{ - LEDGER_SHUTDOWN, LEDGER_STARTED, NON_VALIDATOR_NODE, TX_ACCEPTED, - TX_APPLIED_SUCCESS, TX_FAILED, TX_REJECTED, VALIDATOR_NODE, + LEDGER_SHUTDOWN, LEDGER_STARTED, NON_VALIDATOR_NODE, TX_APPLIED_SUCCESS, + TX_FAILED, TX_REJECTED, VALIDATOR_NODE, }; use crate::{run, run_as}; @@ -579,10 +579,6 @@ fn ledger_txs_and_queries() -> Result<()> { tx_args.clone() }; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - - if !dry_run { - client.exp_string(TX_ACCEPTED)?; - } client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); } @@ -753,7 +749,6 @@ fn wrapper_disposable_signer() -> Result<()> { &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, tx_args, Some(720))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; } @@ -792,7 +787,6 @@ fn wrapper_disposable_signer() -> Result<()> { ]; let mut client = run!(test, Bin::Client, tx_args, Some(720))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; let tx_args = vec!["shielded-sync", "--node", &validator_one_rpc]; @@ -899,7 +893,6 @@ fn invalid_transactions() -> Result<()> { ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_REJECTED)?; client.assert_success(); @@ -951,7 +944,6 @@ fn invalid_transactions() -> Result<()> { ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_FAILED)?; client.assert_success(); Ok(()) @@ -1803,7 +1795,6 @@ fn ledger_many_txs_in_a_block() -> Result<()> { let mut args = (*tx_args).clone(); args.push(&*validator_one_rpc); let mut client = run!(*test, Bin::Client, args, Some(80))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); let res: Result<()> = Ok(()); @@ -3303,6 +3294,13 @@ fn deactivate_and_reactivate_validator() -> Result<()> { ethereum_bridge::ledger::Mode::Off, None, ); + set_ethereum_bridge_mode( + &test, + &test.net.chain_id, + Who::Validator(1), + ethereum_bridge::ledger::Mode::Off, + None, + ); // 1. Run the ledger node let _bg_validator_0 = @@ -3815,6 +3813,7 @@ fn change_consensus_key() -> Result<()> { Ok(()) } + #[test] fn proposal_change_shielded_reward() -> Result<()> { let test = setup::network( diff --git a/crates/tests/src/strings.rs b/crates/tests/src/strings.rs index f0b1f8ea82..0a9223dcc5 100644 --- a/crates/tests/src/strings.rs +++ b/crates/tests/src/strings.rs @@ -21,9 +21,6 @@ pub const TX_REJECTED: &str = "Transaction was rejected by VPs"; /// Inner transaction failed in execution (no VPs ran). pub const TX_FAILED: &str = "Transaction failed"; -/// Wrapper transaction accepted. -pub const TX_ACCEPTED: &str = "Wrapper transaction accepted"; - pub const WALLET_HD_PASSPHRASE_PROMPT: &str = "Enter BIP39 passphrase (empty for none): "; From 32157be770a5d6357f2ac8927cd3960085e6a05d Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 16 Feb 2024 12:58:42 +0100 Subject: [PATCH 05/17] added changelog --- .changelog/unreleased/improvements/2627-remove-tx-queue.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/unreleased/improvements/2627-remove-tx-queue.md diff --git a/.changelog/unreleased/improvements/2627-remove-tx-queue.md b/.changelog/unreleased/improvements/2627-remove-tx-queue.md new file mode 100644 index 0000000000..989d7824e8 --- /dev/null +++ b/.changelog/unreleased/improvements/2627-remove-tx-queue.md @@ -0,0 +1,3 @@ +- Instead of having every user tx be executed across two blocks, the first executing a wrapper and the + second executing the main payload, this change makes it so that the entire tx is executed in a single + block (or rejected). ([\#2627](https://github.com/anoma/namada/pull/2627)) \ No newline at end of file From 816db3901a0bacf5393a53a35212d07e32463f4f Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 16 Feb 2024 17:25:58 +0100 Subject: [PATCH 06/17] Small fixes --- .../lib/node/ledger/shell/finalize_block.rs | 19 ++++++++++++++----- crates/tests/src/e2e/ibc_tests.rs | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index 17825b18bf..c09d0c49e0 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -395,6 +395,15 @@ where tx_event["hash"], result.vps_result.rejected_vps ); + // The fee unshield operation could still have been + // committed + if wrapper_args + .map(|args| args.is_committed_fee_unshield) + .unwrap_or_default() + { + tx_event["is_valid_masp_tx"] = + format!("{}", tx_index); + } // If an inner tx failed for any reason but invalid // signature, commit its hash to storage, otherwise @@ -476,12 +485,12 @@ where tx_event["code"] = ResultCode::InvalidTx.into(); // The fee unshield operation could still have been // committed - if let Some(WrapperArgs { - is_committed_fee_unshield: true, - .. - }) = wrapper_args + if wrapper_args + .map(|args| args.is_committed_fee_unshield) + .unwrap_or_default() { - tx_event["is_valid_masp_tx"] = format!("{}", tx_index); + tx_event["is_valid_masp_tx"] = + format!("{}", tx_index); } tx_event["code"] = ResultCode::WasmRuntimeError.into(); } diff --git a/crates/tests/src/e2e/ibc_tests.rs b/crates/tests/src/e2e/ibc_tests.rs index 8b2211a45b..4e079b8c38 100644 --- a/crates/tests/src/e2e/ibc_tests.rs +++ b/crates/tests/src/e2e/ibc_tests.rs @@ -205,7 +205,7 @@ fn run_ledger_ibc() -> Result<()> { } #[test] -fn drun_ledger_ibc_with_hermes() -> Result<()> { +fn run_ledger_ibc_with_hermes() -> Result<()> { let update_genesis = |mut genesis: templates::All, base_dir: &_| { genesis.parameters.parameters.epochs_per_year = 31536; From 93888b9519cb2b5375c82f5bf1e172012310f652 Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 19 Feb 2024 11:46:38 +0100 Subject: [PATCH 07/17] [fix]: Fix benchmarks --- crates/apps/src/lib/bench_utils.rs | 19 ++++++++++++++----- .../lib/node/ledger/shell/finalize_block.rs | 3 +-- crates/tx/src/data/wrapper.rs | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/apps/src/lib/bench_utils.rs b/crates/apps/src/lib/bench_utils.rs index b6ff9229bc..88384e828a 100644 --- a/crates/apps/src/lib/bench_utils.rs +++ b/crates/apps/src/lib/bench_utils.rs @@ -71,7 +71,7 @@ use namada::ledger::queries::{ }; use namada::state::StorageRead; use namada::tx::data::pos::Bond; -use namada::tx::data::{TxResult, VpsResult}; +use namada::tx::data::{Fee, TxResult, VpsResult}; use namada::tx::{Code, Data, Section, Signature, Tx}; use namada::vm::wasm::run; use namada::{proof_of_stake, tendermint}; @@ -289,9 +289,7 @@ impl BenchShell { extra_sections: Option>, signers: Vec<&SecretKey>, ) -> Tx { - let mut tx = Tx::from_type(namada::tx::data::TxType::Decrypted( - namada::tx::data::DecryptedTx::Decrypted, - )); + let mut tx = Tx::from_type(namada::tx::data::TxType::Raw); // NOTE: here we use the code hash to avoid including the cost for the // wasm validation. The wasm codes (both txs and vps) are always @@ -564,7 +562,18 @@ impl BenchShell { // Commit a masp transaction and cache the tx and the changed keys for // client queries - pub fn commit_masp_tx(&mut self, masp_tx: Tx) { + pub fn commit_masp_tx(&mut self, mut masp_tx: Tx) { + use namada::core::types::key::RefTo; + masp_tx.add_wrapper( + Fee { + amount_per_gas_unit: DenominatedAmount::native(0.into()), + token: self.wl_storage.storage.native_token.clone(), + }, + defaults::albert_keypair().ref_to(), + self.wl_storage.storage.last_epoch, + 0.into(), + None, + ); self.last_block_masp_txs .push((masp_tx, self.state.write_log().get_keys())); self.state.commit_tx(); diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index c09d0c49e0..20b3c47518 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -489,8 +489,7 @@ where .map(|args| args.is_committed_fee_unshield) .unwrap_or_default() { - tx_event["is_valid_masp_tx"] = - format!("{}", tx_index); + tx_event["is_valid_masp_tx"] = format!("{}", tx_index); } tx_event["code"] = ResultCode::WasmRuntimeError.into(); } diff --git a/crates/tx/src/data/wrapper.rs b/crates/tx/src/data/wrapper.rs index a70ebaa39d..4f6fcd3dae 100644 --- a/crates/tx/src/data/wrapper.rs +++ b/crates/tx/src/data/wrapper.rs @@ -194,6 +194,7 @@ pub mod wrapper_tx { pub pk: common::PublicKey, /// The epoch in which the tx is to be submitted. This determines /// which decryption key will be used + /// TODO: Is this still necessary without the DKG? Seems not pub epoch: Epoch, /// Max amount of gas that can be used when executing the inner tx pub gas_limit: GasLimit, From bf8d573aad2d12c6c342ba0482f2327070cc7022 Mon Sep 17 00:00:00 2001 From: Jacob Turner Date: Tue, 20 Feb 2024 13:13:10 +0100 Subject: [PATCH 08/17] Update crates/tx/src/data/wrapper.rs Co-authored-by: Tiago Carvalho --- crates/tx/src/data/wrapper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tx/src/data/wrapper.rs b/crates/tx/src/data/wrapper.rs index 4f6fcd3dae..705a88ec34 100644 --- a/crates/tx/src/data/wrapper.rs +++ b/crates/tx/src/data/wrapper.rs @@ -194,7 +194,7 @@ pub mod wrapper_tx { pub pk: common::PublicKey, /// The epoch in which the tx is to be submitted. This determines /// which decryption key will be used - /// TODO: Is this still necessary without the DKG? Seems not + // TODO: Is this still necessary without the DKG? Seems not pub epoch: Epoch, /// Max amount of gas that can be used when executing the inner tx pub gas_limit: GasLimit, From a106856fe26dcb62c54255005aeadb61462d26ea Mon Sep 17 00:00:00 2001 From: Jacob Turner Date: Tue, 20 Feb 2024 13:13:35 +0100 Subject: [PATCH 09/17] Update crates/apps/src/lib/node/ledger/shell/block_alloc.rs Co-authored-by: Tiago Carvalho --- crates/apps/src/lib/node/ledger/shell/block_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index 1d387bd7d6..cc44ce20e0 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -146,7 +146,7 @@ where impl BlockAllocator> { /// Construct a new [`BlockAllocator`], with an upper bound - /// on the max size of all txs in a block defined by Tendermint and an upper + /// on the max size of all txs in a block defined by CometBFT and an upper /// bound on the max gas in a block. #[inline] pub fn init( From b3734026c21933f72e2b558c9a7fab9fe8745887 Mon Sep 17 00:00:00 2001 From: Jacob Turner Date: Tue, 20 Feb 2024 13:13:46 +0100 Subject: [PATCH 10/17] Update crates/apps/src/lib/node/ledger/shell/block_alloc.rs Co-authored-by: Tiago Carvalho --- crates/apps/src/lib/node/ledger/shell/block_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index cc44ce20e0..f6725eb760 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -150,7 +150,7 @@ impl BlockAllocator> { /// bound on the max gas in a block. #[inline] pub fn init( - tendermint_max_block_space_in_bytes: u64, + cometbft_max_block_space_in_bytes: u64, max_block_gas: u64, ) -> Self { let max = tendermint_max_block_space_in_bytes; From e3481c8f53f6c384c6489dfa2d907e3efd24a07b Mon Sep 17 00:00:00 2001 From: Jacob Turner Date: Tue, 20 Feb 2024 13:13:54 +0100 Subject: [PATCH 11/17] Update crates/apps/src/lib/node/ledger/shell/block_alloc.rs Co-authored-by: Tiago Carvalho --- crates/apps/src/lib/node/ledger/shell/block_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index f6725eb760..14c9fa4f31 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -326,7 +326,7 @@ pub mod threshold { } } - /// Divide free space in three. + /// Divide free space in half. pub const ONE_HALF: Threshold = Threshold::new(1, 2); } From a8abe5615ad1b57a63e8e68841aee801f0503e72 Mon Sep 17 00:00:00 2001 From: Jacob Turner Date: Tue, 20 Feb 2024 13:14:07 +0100 Subject: [PATCH 12/17] Update crates/apps/src/lib/node/ledger/shell/block_alloc.rs Co-authored-by: Tiago Carvalho --- crates/apps/src/lib/node/ledger/shell/block_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index 14c9fa4f31..550845fee9 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -348,7 +348,7 @@ mod tests { type BsaInitialProtocolTxs = BlockAllocator>; - /// Convenience alias for a block space allocator at a state with protocol + /// Convenience alias for a block allocator at a state with protocol /// txs. type BsaNormalTxs = BlockAllocator; From 569277b2367fcfe67a258c596ec43bfd405aacab Mon Sep 17 00:00:00 2001 From: Jacob Turner Date: Tue, 20 Feb 2024 13:16:04 +0100 Subject: [PATCH 13/17] Update crates/apps/src/lib/node/ledger/shell/block_alloc.rs Co-authored-by: Tiago Carvalho --- crates/apps/src/lib/node/ledger/shell/block_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index 550845fee9..cc07deaeea 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -365,7 +365,7 @@ mod tests { /// reserved for each kind of tx type, in the /// allocator's common path. Further check that /// if not enough normal txs are present, the rest - /// if filled with protocol txs + /// is filled with protocol txs #[test] fn test_filling_up_with_protocol() { const BLOCK_SIZE: u64 = 60; From 2a3bf58a04e47879aca47f99229d73b1eb398449 Mon Sep 17 00:00:00 2001 From: satan Date: Tue, 20 Feb 2024 14:15:09 +0100 Subject: [PATCH 14/17] Addressing review comments --- Cargo.toml | 1 + crates/apps/Cargo.toml | 2 +- .../src/lib/node/ledger/shell/block_alloc.rs | 45 ++++++++++--------- .../node/ledger/shell/block_alloc/states.rs | 6 +-- .../shell/block_alloc/states/normal_txs.rs | 6 +-- .../shell/block_alloc/states/protocol_txs.rs | 4 +- .../lib/node/ledger/shell/finalize_block.rs | 25 ++++++++++- .../lib/node/ledger/shell/prepare_proposal.rs | 5 ++- .../lib/node/ledger/shell/process_proposal.rs | 4 +- .../src/lib/node/ledger/shell/testing/node.rs | 2 +- crates/core/src/storage.rs | 26 ++++++++++- crates/namada/src/ledger/native_vp/masp.rs | 11 ++--- crates/namada/src/ledger/protocol/mod.rs | 2 +- 13 files changed, 93 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1e02a8eda6..1457e7d640 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ data-encoding = "2.3.2" derivation-path = "0.2.0" derivative = "2.2.0" directories = "4.0.1" +drain_filter_polyfill = "0.1.3" ed25519-consensus = "1.2.0" escargot = "0.5.7" ethabi = "18.0.0" diff --git a/crates/apps/Cargo.toml b/crates/apps/Cargo.toml index c9e7fe0135..32f1cae83c 100644 --- a/crates/apps/Cargo.toml +++ b/crates/apps/Cargo.toml @@ -84,7 +84,7 @@ config.workspace = true data-encoding.workspace = true derivative.workspace = true directories.workspace = true -drain_filter_polyfill = "0.1.3" +drain_filter_polyfill.workspace = true ed25519-consensus = { workspace = true, features = ["std"] } ethabi.workspace = true ethbridge-bridge-events.workspace = true diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs index cc07deaeea..625712cfe0 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -153,7 +153,7 @@ impl BlockAllocator> { cometbft_max_block_space_in_bytes: u64, max_block_gas: u64, ) -> Self { - let max = tendermint_max_block_space_in_bytes; + let max = cometbft_max_block_space_in_bytes; Self { _state: PhantomData, block: TxBin::init(max), @@ -166,7 +166,7 @@ impl BlockAllocator> { } } -impl BlockAllocator { +impl BlockAllocator { /// Construct a new [`BlockAllocator`], with an upper bound /// on the max size of all txs in a block defined by Tendermint and an upper /// bound on the max gas in a block. @@ -332,13 +332,12 @@ pub mod threshold { #[cfg(test)] mod tests { - use std::cell::RefCell; use assert_matches::assert_matches; use proptest::prelude::*; use super::states::{ - BuildingProtocolTxBatch, BuildingTxBatch, NextState, TryAlloc, + BuildingNormalTxBatch, BuildingProtocolTxBatch, NextState, TryAlloc, }; use super::*; use crate::node::ledger::shims::abcipp_shim_types::shim::TxBytes; @@ -350,7 +349,7 @@ mod tests { /// Convenience alias for a block allocator at a state with protocol /// txs. - type BsaNormalTxs = BlockAllocator; + type BsaNormalTxs = BlockAllocator; /// Proptest generated txs. #[derive(Debug)] @@ -416,7 +415,7 @@ mod tests { // reserve block space for protocol txs let mut alloc = BsaInitialProtocolTxs::init(BLOCK_SIZE, BLOCK_GAS); - // allocate ~1/3 of the block space to encrypted txs + // allocate ~1/3 of the block space to protocol txs assert!(alloc.try_alloc(&[0; 18]).is_ok()); // reserve block space for normal txs @@ -436,6 +435,12 @@ mod tests { alloc.try_alloc(BlockResources::new(&[0; 1], 0)), Err(AllocFailure::Rejected { .. }) ); + + let mut alloc = alloc.next_state(); + assert_matches!( + alloc.try_alloc(&[0; 1]), + Err(AllocFailure::OverflowsBin { .. }) + ); } proptest! { @@ -518,15 +523,15 @@ mod tests { // iterate over the produced txs to make sure we can keep // dumping new txs without filling up the bins - let bins = RefCell::new(BsaInitialProtocolTxs::init( + let mut bins = BsaInitialProtocolTxs::init( tendermint_max_block_space_in_bytes, max_block_gas, - )); + ); let mut protocol_tx_iter = protocol_txs.iter(); let mut allocated_txs = vec![]; let mut new_size = 0; for tx in protocol_tx_iter.by_ref() { - let bin = bins.borrow().protocol_txs; + let bin = bins.protocol_txs; if new_size + tx.len() as u64 >= bin.allotted { break; } else { @@ -535,14 +540,14 @@ mod tests { } } for tx in allocated_txs { - assert!(bins.borrow_mut().try_alloc(tx).is_ok()); + assert!(bins.try_alloc(tx).is_ok()); } - let bins = RefCell::new(bins.into_inner().next_state()); - let mut new_size = bins.borrow().normal_txs.space.allotted; + let mut bins = bins.next_state(); + let mut new_size = bins.normal_txs.space.allotted; let mut decrypted_txs = vec![]; for tx in normal_txs { - let bin = bins.borrow().normal_txs.space; + let bin = bins.normal_txs.space; if (new_size + tx.len() as u64) < bin.allotted { new_size += tx.len() as u64; decrypted_txs.push(tx); @@ -551,18 +556,14 @@ mod tests { } } for tx in decrypted_txs { - assert!( - bins.borrow_mut() - .try_alloc(BlockResources::new(&tx, 0)) - .is_ok() - ); + assert!(bins.try_alloc(BlockResources::new(&tx, 0)).is_ok()); } - let bins = RefCell::new(bins.into_inner().next_state()); + let mut bins = bins.next_state(); let mut allocated_txs = vec![]; - let mut new_size = bins.borrow().protocol_txs.allotted; + let mut new_size = bins.protocol_txs.allotted; for tx in protocol_tx_iter.by_ref() { - let bin = bins.borrow().protocol_txs; + let bin = bins.protocol_txs; if new_size + tx.len() as u64 >= bin.allotted { break; } else { @@ -572,7 +573,7 @@ mod tests { } for tx in allocated_txs { - assert!(bins.borrow_mut().try_alloc(tx).is_ok()); + assert!(bins.try_alloc(tx).is_ok()); } } diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs index 64587d5464..ad2d517d85 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs @@ -8,7 +8,7 @@ //! //! 1. [`BuildingProtocolTxBatch`] - the initial state. In //! this state, we populate a block with protocol txs. -//! 2. [`BuildingTxBatch`] - the second state. In +//! 2. [`BuildingNormalTxBatch`] - the second state. In //! this state, we populate a block with non-protocol txs. //! 3. [`BuildingProtocolTxBatch`] - we return to this state to //! fill up any remaining block space if possible. @@ -28,7 +28,7 @@ use super::AllocFailure; /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. pub struct BuildingProtocolTxBatch { - /// One of [`WithEncryptedTxs`] and [`WithoutEncryptedTxs`]. + /// One of [`WithNormalTxs`] and [`WithoutNormalTxs`]. _mode: Mode, } @@ -49,7 +49,7 @@ pub enum WithoutNormalTxs {} /// /// For more info, read the module docs of /// [`crate::node::ledger::shell::block_alloc::states`]. -pub struct BuildingTxBatch {} +pub struct BuildingNormalTxBatch {} /// Try to allocate a new transaction on a `BlockAllocator` state. /// diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs index 680b06a8dc..e15333216f 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/normal_txs.rs @@ -2,12 +2,12 @@ use std::marker::PhantomData; use super::super::{AllocFailure, BlockAllocator, TxBin}; use super::{ - BuildingProtocolTxBatch, BuildingTxBatch, NextStateImpl, TryAlloc, + BuildingNormalTxBatch, BuildingProtocolTxBatch, NextStateImpl, TryAlloc, WithoutNormalTxs, }; use crate::node::ledger::shell::block_alloc::BlockResources; -impl TryAlloc for BlockAllocator { +impl TryAlloc for BlockAllocator { type Resources<'tx> = BlockResources<'tx>; #[inline] @@ -20,7 +20,7 @@ impl TryAlloc for BlockAllocator { } } -impl NextStateImpl for BlockAllocator { +impl NextStateImpl for BlockAllocator { type Next = BlockAllocator>; #[inline] diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs index 570130b208..302dc83824 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use super::super::{AllocFailure, BlockAllocator}; use super::{ - BuildingProtocolTxBatch, BuildingTxBatch, NextStateImpl, TryAlloc, + BuildingNormalTxBatch, BuildingProtocolTxBatch, NextStateImpl, TryAlloc, WithNormalTxs, }; use crate::node::ledger::shell::block_alloc::TxBin; @@ -20,7 +20,7 @@ impl TryAlloc for BlockAllocator> { } impl NextStateImpl for BlockAllocator> { - type Next = BlockAllocator; + type Next = BlockAllocator; #[inline] fn next_state_impl(mut self) -> Self::Next { diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index 20b3c47518..04bb26f215 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -230,7 +230,9 @@ where }), ) } - TxType::Decrypted(_) => unreachable!(), + TxType::Decrypted(_) => { + unreachable!("Received decrypted tx in FinalizeBlock") + } TxType::Raw => { tracing::error!( "Internal logic error: FinalizeBlock received a \ @@ -889,6 +891,27 @@ mod test_finalize_block { assert_eq!(code, &String::from(ResultCode::InvalidTx)); } + #[test] + #[should_panic(expected = "Received decrypted tx in FinalizeBlock")] + fn test_decrypted_is_unreachable() { + const LAST_HEIGHT: BlockHeight = BlockHeight(3); + let (mut shell, _, _, _) = setup_at_height(LAST_HEIGHT); + let tx = Tx::from_type(TxType::Decrypted(DecryptedTx::Undecryptable)) + .to_bytes(); + let processed_tx = ProcessedTx { + tx: tx.into(), + result: TxResult { + code: ResultCode::Ok.into(), + info: "".into(), + }, + }; + let req = FinalizeBlock { + txs: vec![processed_tx], + ..Default::default() + }; + _ = shell.finalize_block(req); + } + /// Test that once a validator's vote for an Ethereum event lands /// on-chain from a vote extension digest, it dequeues from the /// list of events to vote on. diff --git a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 956d0d9404..4ce342dec4 100644 --- a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -15,7 +15,7 @@ use namada::vm::WasmCacheAccess; use super::super::*; use super::block_alloc::states::{ - BuildingProtocolTxBatch, BuildingTxBatch, NextState, TryAlloc, + BuildingNormalTxBatch, BuildingProtocolTxBatch, NextState, TryAlloc, WithNormalTxs, WithoutNormalTxs, }; use super::block_alloc::{AllocFailure, BlockAllocator, BlockResources}; @@ -104,7 +104,7 @@ where /// Tendermint's mempool. fn build_normal_txs( &self, - mut alloc: BlockAllocator, + mut alloc: BlockAllocator, txs: &[TxBytes], block_time: Option, block_proposer: &Address, @@ -229,6 +229,7 @@ where ) ) .collect(); + // avoid dropping the txs that couldn't be included in the block deserialized_iter.keep_rest(); (alloc, taken) } diff --git a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs index adc7de1ac2..fadd15a252 100644 --- a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -403,7 +403,9 @@ where }, } } - TxType::Decrypted(_) => unreachable!(), + TxType::Decrypted(_) => { + unreachable!("Received decrypted tx in ProcessProposal") + } TxType::Wrapper(wrapper) => { // Account for gas and space. This is done even if the // transaction is later deemed invalid, to diff --git a/crates/apps/src/lib/node/ledger/shell/testing/node.rs b/crates/apps/src/lib/node/ledger/shell/testing/node.rs index 0d75470964..d2718740c6 100644 --- a/crates/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/crates/apps/src/lib/node/ledger/shell/testing/node.rs @@ -941,7 +941,7 @@ fn parse_tm_query( query: namada::tendermint_rpc::query::Query, ) -> dumb_queries::QueryMatcher { const QUERY_PARSING_REGEX_STR: &str = - r"^tm\.event='NewBlock' AND (applied)\.hash='([^']+)'$"; + r"^tm\.event='NewBlock' AND applied\.hash='([^']+)'$"; lazy_static! { /// Compiled regular expression used to parse Tendermint queries. diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index 71b0fd347b..ada97688bc 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -1468,11 +1468,33 @@ pub struct IndexedTx { pub height: BlockHeight, /// The index in the block of the tx pub index: TxIndex, - /// A transcation can have up to two sheilded transfers. - /// This indicates if the wrapper contained a sheilded transfer. + /// A transcation can have up to two shielded transfers. + /// This indicates if the wrapper contained a shielded transfer. pub is_wrapper: bool, } +#[derive( + Default, + Debug, + Copy, + Clone, + BorshSerialize, + BorshDeserialize, + Eq, + PartialEq, + Ord, + PartialOrd, +)] +/// The index of a tx to be stored in the DB. Differs from +/// [`IndexedTx`] as we know we only persist txs which aren't +/// wrapper txs. +pub struct StoredIndexedTx { + /// The block height of the indexed tx + pub height: BlockHeight, + /// The index in the block of the tx + pub index: TxIndex, +} + #[cfg(test)] /// Tests and strategies for storage pub mod tests { diff --git a/crates/namada/src/ledger/native_vp/masp.rs b/crates/namada/src/ledger/native_vp/masp.rs index 690839b7fb..67a9c64a7d 100644 --- a/crates/namada/src/ledger/native_vp/masp.rs +++ b/crates/namada/src/ledger/native_vp/masp.rs @@ -265,14 +265,11 @@ where 1 => { match self .ctx - .read_post::(pin_keys.first().unwrap())? + .read_post::(pin_keys.first().unwrap())? { - Some(IndexedTx { - height, - index, - is_wrapper: false, - }) if height == self.ctx.get_block_height()? - && index == self.ctx.get_tx_index()? => {} + Some(StoredIndexedTx { height, index }) + if height == self.ctx.get_block_height()? + && index == self.ctx.get_tx_index()? => {} Some(_) => { return Err(Error::NativeVpError( native_vp::Error::SimpleMessage( diff --git a/crates/namada/src/ledger/protocol/mod.rs b/crates/namada/src/ledger/protocol/mod.rs index 0507cc26ea..724f52208c 100644 --- a/crates/namada/src/ledger/protocol/mod.rs +++ b/crates/namada/src/ledger/protocol/mod.rs @@ -218,7 +218,7 @@ where inner_res.wrapper_changed_keys = changed_keys; Ok(inner_res) } - _ => Ok(TxResult::default()), + TxType::Decrypted(_) => Err(Error::TxTypeError), } } From 57a599c4e664957a9ba4f9164bec67c472086a7a Mon Sep 17 00:00:00 2001 From: satan Date: Tue, 20 Feb 2024 18:06:25 +0100 Subject: [PATCH 15/17] addressing review comments --- .../lib/node/ledger/shell/prepare_proposal.rs | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 4ce342dec4..1426d75f6a 100644 --- a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -49,9 +49,9 @@ where { // start counting allotted space for txs let alloc = self.get_protocol_txs_allocator(); - // add vote extension protocol txs - let (alloc, mut txs) = self.build_protocol_txs(alloc, &mut req.txs); - let alloc = alloc.next_state(); + // add initial protocol txs + let (alloc, mut txs) = + self.build_protocol_tx_with_normal_txs(alloc, &mut req.txs); // add encrypted txs let tm_raw_hash_string = @@ -73,9 +73,8 @@ where local_config.as_ref(), ); txs.append(&mut normal_txs); - // decrypt the wrapper txs included in the previous block - let (_, mut remaining_txs) = - self.build_protocol_txs(alloc, &mut req.txs); + let mut remaining_txs = + self.build_protocol_tx_without_normal_txs(alloc, &mut req.txs); txs.append(&mut remaining_txs); txs } else { @@ -101,7 +100,7 @@ where } /// Builds a batch of encrypted transactions, retrieved from - /// Tendermint's mempool. + /// CometBFT's mempool. fn build_normal_txs( &self, mut alloc: BlockAllocator, @@ -173,6 +172,28 @@ where (txs, alloc) } + /// Allocate an initial set of protocol txs and advance to the + /// next allocation state. + fn build_protocol_tx_with_normal_txs( + &self, + alloc: BlockAllocator>, + txs: &mut Vec, + ) -> (BlockAllocator, Vec) { + let (alloc, txs) = self.build_protocol_txs(alloc, txs); + (alloc.next_state(), txs) + } + + /// Allocate protocol txs into any remaining space. After this, no + /// more allocation will take place. + fn build_protocol_tx_without_normal_txs( + &self, + alloc: BlockAllocator>, + txs: &mut Vec, + ) -> Vec { + let (_, txs) = self.build_protocol_txs(alloc, txs); + txs + } + /// Builds a batch of protocol transactions. fn build_protocol_txs( &self, From 4770db6ec023c9bb199ab256cc4dba0c7a68da91 Mon Sep 17 00:00:00 2001 From: satan Date: Sat, 2 Mar 2024 17:29:56 +0100 Subject: [PATCH 16/17] Rebased. Might require more fixes but unit tests are passing --- crates/apps/src/lib/bench_utils.rs | 14 +- .../node/ledger/shell/block_alloc/states.rs | 12 +- .../lib/node/ledger/shell/finalize_block.rs | 144 ++++++++---------- crates/apps/src/lib/node/ledger/shell/mod.rs | 11 -- .../lib/node/ledger/shell/prepare_proposal.rs | 25 ++- .../lib/node/ledger/shell/process_proposal.rs | 5 +- .../src/lib/node/ledger/shell/testing/node.rs | 2 +- .../lib/node/ledger/shell/vote_extensions.rs | 22 ++- .../src/lib/node/ledger/storage/rocksdb.rs | 8 +- crates/benches/native_vps.rs | 10 +- crates/core/src/event.rs | 4 - crates/namada/src/ledger/mod.rs | 13 +- crates/namada/src/ledger/native_vp/masp.rs | 2 +- crates/namada/src/ledger/protocol/mod.rs | 15 +- crates/sdk/src/lib.rs | 1 - crates/sdk/src/masp.rs | 7 +- crates/state/src/in_memory.rs | 5 +- crates/state/src/lib.rs | 2 +- crates/state/src/wl_state.rs | 9 +- crates/state/src/write_log.rs | 27 +--- crates/storage/src/tx_queue.rs | 47 ------ crates/tests/src/e2e/ledger_tests.rs | 1 - crates/tx/src/data/mod.rs | 73 --------- crates/tx/src/data/wrapper.rs | 5 +- crates/tx/src/lib.rs | 15 +- crates/tx/src/types.rs | 13 +- 26 files changed, 127 insertions(+), 365 deletions(-) diff --git a/crates/apps/src/lib/bench_utils.rs b/crates/apps/src/lib/bench_utils.rs index 88384e828a..56f2ffd1ee 100644 --- a/crates/apps/src/lib/bench_utils.rs +++ b/crates/apps/src/lib/bench_utils.rs @@ -329,9 +329,7 @@ impl BenchShell { pub fn generate_ibc_tx(&self, wasm_code_path: &str, msg: impl Msg) -> Tx { // This function avoid serializaing the tx data with Borsh - let mut tx = Tx::from_type(namada::tx::data::TxType::Decrypted( - namada::tx::data::DecryptedTx::Decrypted, - )); + let mut tx = Tx::from_type(namada::tx::data::TxType::Raw); let code_hash = self .read_storage_key(&Key::wasm_hash(wasm_code_path)) .unwrap(); @@ -563,14 +561,14 @@ impl BenchShell { // Commit a masp transaction and cache the tx and the changed keys for // client queries pub fn commit_masp_tx(&mut self, mut masp_tx: Tx) { - use namada::core::types::key::RefTo; + use namada::core::key::RefTo; masp_tx.add_wrapper( Fee { amount_per_gas_unit: DenominatedAmount::native(0.into()), - token: self.wl_storage.storage.native_token.clone(), + token: self.state.in_mem().native_token.clone(), }, defaults::albert_keypair().ref_to(), - self.wl_storage.storage.last_epoch, + self.state.in_mem().last_epoch, 0.into(), None, ); @@ -584,9 +582,7 @@ pub fn generate_foreign_key_tx(signer: &SecretKey) -> Tx { let wasm_code = std::fs::read("../../wasm_for_tests/tx_write.wasm").unwrap(); - let mut tx = Tx::from_type(namada::tx::data::TxType::Decrypted( - namada::tx::data::DecryptedTx::Decrypted, - )); + let mut tx = Tx::from_type(namada::tx::data::TxType::Raw); tx.set_code(Code::new(wasm_code, None)); tx.set_data(Data::new( TxWriteData { diff --git a/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs b/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs index ad2d517d85..ed5d5e3004 100644 --- a/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs +++ b/crates/apps/src/lib/node/ledger/shell/block_alloc/states.rs @@ -6,12 +6,12 @@ //! //! The state machine moves through the following state DAG: //! -//! 1. [`BuildingProtocolTxBatch`] - the initial state. In -//! this state, we populate a block with protocol txs. -//! 2. [`BuildingNormalTxBatch`] - the second state. In -//! this state, we populate a block with non-protocol txs. -//! 3. [`BuildingProtocolTxBatch`] - we return to this state to -//! fill up any remaining block space if possible. +//! 1. [`BuildingProtocolTxBatch`] - the initial state. In this state, we +//! populate a block with protocol txs. +//! 2. [`BuildingNormalTxBatch`] - the second state. In this state, we populate +//! a block with non-protocol txs. +//! 3. [`BuildingProtocolTxBatch`] - we return to this state to fill up any +//! remaining block space if possible. mod normal_txs; mod protocol_txs; diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index 04bb26f215..e1ba3e73ce 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -5,7 +5,7 @@ use masp_primitives::merkle_tree::CommitmentTree; use masp_primitives::sapling::Node; use namada::core::storage::{BlockHash, BlockResults, Epoch, Header}; use namada::governance::pgf::inflation as pgf_inflation; -use namada::ledger::events::EventType; +use namada::hash::Hash; use namada::ledger::gas::GasMetering; use namada::ledger::pos::namada_proof_of_stake; use namada::ledger::protocol::WrapperArgs; @@ -204,11 +204,8 @@ where continue; } - let ( - mut tx_event, - mut tx_gas_meter, - mut wrapper_args - ) = match &tx_header.tx_type { + let (mut tx_event, tx_gas_meter, mut wrapper_args) = + match &tx_header.tx_type { TxType::Wrapper(wrapper) => { stats.increment_wrapper_txs(); let tx_event = new_tx_event(&tx, height.0); @@ -230,9 +227,6 @@ where }), ) } - TxType::Decrypted(_) => { - unreachable!("Received decrypted tx in FinalizeBlock") - } TxType::Raw => { tracing::error!( "Internal logic error: FinalizeBlock received a \ @@ -255,60 +249,61 @@ where &tx, ) .unwrap(); - if self - .mode - .get_validator_address() - .map(|validator| { - validator == &ext.data.validator_addr - }) - .unwrap_or(false) - { - for event in ext.data.ethereum_events.iter() { - self.mode.dequeue_eth_event(event); + if self + .mode + .get_validator_address() + .map(|validator| { + validator == &ext.data.validator_addr + }) + .unwrap_or(false) + { + for event in ext.data.ethereum_events.iter() { + self.mode.dequeue_eth_event(event); + } } + ( + new_tx_event(&tx, height.0), + TxGasMeter::new_from_sub_limit(0.into()), + None, + ) } - ( - new_tx_event(&tx, height.0), - TxGasMeter::new_from_sub_limit(0.into()), - None, - ) - } - ProtocolTxType::EthereumEvents => { - let digest = + ProtocolTxType::EthereumEvents => { + let digest = ethereum_tx_data_variants::EthereumEvents::try_from( &tx, ).unwrap(); - if let Some(address) = - self.mode.get_validator_address().cloned() - { - let this_signer = &( - address, - self.state.in_mem().get_last_block_height(), - ); - for MultiSignedEthEvent { event, signers } in - &digest.events + if let Some(address) = + self.mode.get_validator_address().cloned() { - if signers.contains(this_signer) { - self.mode.dequeue_eth_event(event); + let this_signer = &( + address, + self.state.in_mem().get_last_block_height(), + ); + for MultiSignedEthEvent { event, signers } in + &digest.events + { + if signers.contains(this_signer) { + self.mode.dequeue_eth_event(event); + } } } + ( + new_tx_event(&tx, height.0), + TxGasMeter::new_from_sub_limit(0.into()), + None, + ) } - ( - new_tx_event(&tx, height.0), - TxGasMeter::new_from_sub_limit(0.into()), - None, - ) - } - }, - }; - let replay_protection_hashes = if matches!(tx_header.tx_type, TxType::Wrapper(_)) { - Some(ReplayProtectionHashes { - raw_header_hash: tx.raw_header_hash(), - header_hash: tx.header_hash(), - }) - } else { - None - }; + }, + }; + let replay_protection_hashes = + if matches!(tx_header.tx_type, TxType::Wrapper(_)) { + Some(ReplayProtectionHashes { + raw_header_hash: tx.raw_header_hash(), + header_hash: tx.header_hash(), + }) + } else { + None + }; let tx_gas_meter = RefCell::new(tx_gas_meter); let tx_result = protocol::check_tx_allowed(&tx, &self.state) .and_then(|()| { @@ -468,11 +463,7 @@ where let header_hash = replay_protection_hashes .expect("This cannot fail") .header_hash; - self.state - .delete_tx_hash(header_hash) - .expect( - "Error while deleting tx hash from storage", - ); + self.state.delete_tx_hash(header_hash); } } @@ -627,14 +618,17 @@ where // the wrapper). Requires the wrapper transaction as argument to recover // both the hashes. fn commit_inner_tx_hash(&mut self, hashes: Option) { - if let Some(ReplayProtectionHashes {raw_header_hash, header_hash}) = hashes { + if let Some(ReplayProtectionHashes { + raw_header_hash, + header_hash, + }) = hashes + { self.state .write_tx_hash(raw_header_hash) .expect("Error while writing tx hash to storage"); self.state .delete_tx_hash(header_hash) - .expect("Error while deleting tx hash from storage"); } } } @@ -891,27 +885,6 @@ mod test_finalize_block { assert_eq!(code, &String::from(ResultCode::InvalidTx)); } - #[test] - #[should_panic(expected = "Received decrypted tx in FinalizeBlock")] - fn test_decrypted_is_unreachable() { - const LAST_HEIGHT: BlockHeight = BlockHeight(3); - let (mut shell, _, _, _) = setup_at_height(LAST_HEIGHT); - let tx = Tx::from_type(TxType::Decrypted(DecryptedTx::Undecryptable)) - .to_bytes(); - let processed_tx = ProcessedTx { - tx: tx.into(), - result: TxResult { - code: ResultCode::Ok.into(), - info: "".into(), - }, - }; - let req = FinalizeBlock { - txs: vec![processed_tx], - ..Default::default() - }; - _ = shell.finalize_block(req); - } - /// Test that once a validator's vote for an Ethereum event lands /// on-chain from a vote extension digest, it dequeues from the /// list of events to vote on. @@ -2575,8 +2548,10 @@ mod test_finalize_block { let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount_per_gas_unit: DenominatedAmount::native(1.into()), - token: shell.state.native_token.clone(), + amount_per_gas_unit: DenominatedAmount::native( + 1.into(), + ), + token: shell.state.in_mem().native_token.clone(), }, keypair.ref_to(), Epoch(0), @@ -2722,6 +2697,7 @@ mod test_finalize_block { assert!( shell .state + .write_log() .has_replay_protection_entry(&valid_wrapper.header_hash()) .unwrap_or_default() ); diff --git a/crates/apps/src/lib/node/ledger/shell/mod.rs b/crates/apps/src/lib/node/ledger/shell/mod.rs index 672f3842ce..f4bb54ee2c 100644 --- a/crates/apps/src/lib/node/ledger/shell/mod.rs +++ b/crates/apps/src/lib/node/ledger/shell/mod.rs @@ -1109,12 +1109,6 @@ where the mempool" ); } - TxType::Decrypted(_) => { - response.code = ResultCode::InvalidTx.into(); - response.log = format!( - "{INVALID_MSG}: Decrypted txs cannot be sent by clients" - ); - } } if response.code == ResultCode::Ok.into() { @@ -1401,16 +1395,11 @@ mod test_utils { use namada::core::keccak::KeccakHash; use namada::core::key::*; use namada::core::storage::{BlockHash, Epoch, Header}; - use namada::core::time::DurationSecs; - use namada::ledger::parameters::{EpochDuration, Parameters}; use namada::proof_of_stake::parameters::PosParams; use namada::proof_of_stake::storage::validator_consensus_key_handle; use namada::state::mockdb::MockDB; use namada::state::{LastBlock, StorageWrite}; use namada::tendermint::abci::types::VoteInfo; - use namada::token::conversion::update_allowed_conversions; - use namada::tx::data::Fee; - use namada::tx::{Code, Data}; use tempfile::tempdir; use tokio::sync::mpsc::{Sender, UnboundedReceiver}; diff --git a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 1426d75f6a..d4a28740fd 100644 --- a/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -2,13 +2,12 @@ use masp_primitives::transaction::Transaction; use namada::core::address::Address; -use namada::core::hints; use namada::core::key::tm_raw_hash_to_string; use namada::gas::TxGasMeter; use namada::ledger::protocol; use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::state::{DBIter, StorageHasher, TempWlState, DB}; -use namada::tx::data::{DecryptedTx, TxType, WrapperTx}; +use namada::tx::data::{TxType, WrapperTx}; use namada::tx::Tx; use namada::vm::wasm::{TxCache, VpCache}; use namada::vm::WasmCacheAccess; @@ -56,15 +55,13 @@ where // add encrypted txs let tm_raw_hash_string = tm_raw_hash_to_string(req.proposer_address); - let block_proposer = find_validator_by_raw_hash( - &self.state, - tm_raw_hash_string, - ) - .unwrap() - .expect( - "Unable to find native validator address of block proposer \ - from tendermint raw hash", - ); + let block_proposer = + find_validator_by_raw_hash(&self.state, tm_raw_hash_string) + .unwrap() + .expect( + "Unable to find native validator address of block \ + proposer from tendermint raw hash", + ); let (mut normal_txs, alloc) = self.build_normal_txs( alloc, &req.txs, @@ -96,7 +93,7 @@ where fn get_protocol_txs_allocator( &self, ) -> BlockAllocator> { - (&self.state).into() + self.state.read_only().into() } /// Builds a batch of encrypted transactions, retrieved from @@ -369,12 +366,10 @@ where mod test_prepare_proposal { use std::collections::BTreeSet; - use borsh_ext::BorshSerializeExt; use namada::core::address; use namada::core::ethereum_events::EthereumEvent; use namada::core::key::RefTo; use namada::core::storage::{BlockHeight, InnerEthEventsQueue}; - use namada::ledger::gas::Gas; use namada::ledger::pos::PosQueries; use namada::proof_of_stake::storage::{ consensus_validator_set_handle, @@ -385,7 +380,7 @@ mod test_prepare_proposal { use namada::state::collections::lazy_map::{NestedSubKey, SubKey}; use namada::token::{read_denom, Amount, DenominatedAmount}; use namada::tx::data::Fee; - use namada::tx::{Code, Data, Header, Section, Signature, Signed}; + use namada::tx::{Code, Data, Section, Signature, Signed}; use namada::vote_ext::{ethereum_events, ethereum_tx_data_variants}; use namada::{replay_protection, token}; use namada_sdk::storage::StorageWrite; diff --git a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs index fadd15a252..37bbea1198 100644 --- a/crates/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/crates/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -131,7 +131,7 @@ where block_proposer: &Address, ) -> Vec { let mut temp_state = self.state.with_temp_write_log(); - let mut metadata = ValidationMeta::from(&self.state.read_only()); + let mut metadata = ValidationMeta::from(self.state.read_only()); let mut vp_wasm_cache = self.vp_wasm_cache.clone(); let mut tx_wasm_cache = self.tx_wasm_cache.clone(); @@ -403,9 +403,6 @@ where }, } } - TxType::Decrypted(_) => { - unreachable!("Received decrypted tx in ProcessProposal") - } TxType::Wrapper(wrapper) => { // Account for gas and space. This is done even if the // transaction is later deemed invalid, to diff --git a/crates/apps/src/lib/node/ledger/shell/testing/node.rs b/crates/apps/src/lib/node/ledger/shell/testing/node.rs index d2718740c6..8c22cac956 100644 --- a/crates/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/crates/apps/src/lib/node/ledger/shell/testing/node.rs @@ -503,7 +503,7 @@ impl MockNode { /// Send a tx through Process Proposal and Finalize Block /// and register the results. - fn submit_txs(&self, txs: Vec>) { + pub fn submit_txs(&self, txs: Vec>) { self.finalize_and_commit(); let (proposer_address, votes) = self.prepare_request(); diff --git a/crates/apps/src/lib/node/ledger/shell/vote_extensions.rs b/crates/apps/src/lib/node/ledger/shell/vote_extensions.rs index 1cfa899d0c..fa6fdbfcf9 100644 --- a/crates/apps/src/lib/node/ledger/shell/vote_extensions.rs +++ b/crates/apps/src/lib/node/ledger/shell/vote_extensions.rs @@ -136,15 +136,11 @@ where Some(EthereumTxData::EthEventsVext(ext)) => { // NB: only propose events with at least // one valid nonce - ext.data - .ethereum_events - .iter() - .any(|event| { - self.state - .ethbridge_queries() - .validate_eth_event_nonce(event) - }) - .then(|| tx_bytes.clone()) + ext.data.ethereum_events.iter().any(|event| { + self.state + .ethbridge_queries() + .validate_eth_event_nonce(event) + }) } Some(EthereumTxData::ValSetUpdateVext(ext)) => { // only include non-stale validator set updates @@ -156,13 +152,13 @@ where // to remove it from the mempool this way, but it // will eventually be evicted, getting replaced // by newer txs. - (!self + let is_seen = self .state .ethbridge_queries() - .valset_upd_seen(ext.data.signing_epoch.next())) - .then(|| tx_bytes.clone()) + .valset_upd_seen(ext.data.signing_epoch.next()); + !is_seen } - _ => None, + _ => false, } }) } diff --git a/crates/apps/src/lib/node/ledger/storage/rocksdb.rs b/crates/apps/src/lib/node/ledger/storage/rocksdb.rs index 989c152d46..1bd88e2072 100644 --- a/crates/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/crates/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -54,7 +54,6 @@ use namada::core::time::DateTimeUtc; use namada::core::{decode, encode, ethereum_events, ethereum_structs}; use namada::eth_bridge::storage::proof::BridgePoolRootProof; use namada::ledger::eth_bridge::storage::bridge_pool; -use namada::ledger::storage::tx_queue::TxQueue; use namada::replay_protection; use namada::state::merkle_tree::{base_tree_key_prefix, subtree_key_prefix}; use namada::state::{ @@ -510,10 +509,9 @@ impl RocksDB { // restarting the chain tracing::info!("Reverting non-height-prepended metadata keys"); batch.put_cf(state_cf, "height", encode(&previous_height)); - for metadata_key in [ - "next_epoch_min_start_height", - "next_epoch_min_start_time", - ] { + for metadata_key in + ["next_epoch_min_start_height", "next_epoch_min_start_time"] + { let previous_key = format!("pred/{}", metadata_key); let previous_value = self .0 diff --git a/crates/benches/native_vps.rs b/crates/benches/native_vps.rs index e9c9708b50..0f724c4f56 100644 --- a/crates/benches/native_vps.rs +++ b/crates/benches/native_vps.rs @@ -966,10 +966,7 @@ fn parameters(c: &mut Criterion) { shell.state.write(&proposal_key, 0).unwrap(); // Return a dummy tx for validation - let mut tx = - Tx::from_type(namada::tx::data::TxType::Decrypted( - namada::tx::data::DecryptedTx::Decrypted, - )); + let mut tx = Tx::from_type(namada::tx::data::TxType::Raw); tx.set_data(namada::tx::Data::new(borsh::to_vec(&0).unwrap())); tx } @@ -1041,10 +1038,7 @@ fn pos(c: &mut Criterion) { shell.state.write(&proposal_key, 0).unwrap(); // Return a dummy tx for validation - let mut tx = - Tx::from_type(namada::tx::data::TxType::Decrypted( - namada::tx::data::DecryptedTx::Decrypted, - )); + let mut tx = Tx::from_type(namada::tx::data::TxType::Raw); tx.set_data(namada::tx::Data::new(borsh::to_vec(&0).unwrap())); tx } diff --git a/crates/core/src/event.rs b/crates/core/src/event.rs index d82121de50..ab0aebbf6b 100644 --- a/crates/core/src/event.rs +++ b/crates/core/src/event.rs @@ -49,8 +49,6 @@ pub struct Event { /// The two types of custom events we currently use #[derive(Clone, Debug, Eq, PartialEq, BorshSerialize, BorshDeserialize)] pub enum EventType { - /// The transaction was accepted to be included in a block - Accepted, /// The transaction was applied during block finalization Applied, /// The IBC transaction was applied during block finalization @@ -66,7 +64,6 @@ pub enum EventType { impl Display for EventType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - EventType::Accepted => write!(f, "accepted"), EventType::Applied => write!(f, "applied"), EventType::Ibc(t) => write!(f, "{}", t), EventType::Proposal => write!(f, "proposal"), @@ -82,7 +79,6 @@ impl FromStr for EventType { fn from_str(s: &str) -> Result { match s { - "accepted" => Ok(EventType::Accepted), "applied" => Ok(EventType::Applied), "proposal" => Ok(EventType::Proposal), "pgf_payments" => Ok(EventType::PgfPayment), diff --git a/crates/namada/src/ledger/mod.rs b/crates/namada/src/ledger/mod.rs index ed59846e93..041a1af407 100644 --- a/crates/namada/src/ledger/mod.rs +++ b/crates/namada/src/ledger/mod.rs @@ -43,14 +43,14 @@ mod dry_run_tx { { use borsh_ext::BorshSerializeExt; use namada_gas::{Gas, GasMetering, TxGasMeter}; - use namada_tx::data::{DecryptedTx, TxType}; + use namada_tx::data::TxType; use namada_tx::Tx; use crate::ledger::protocol::ShellParams; use crate::storage::TxIndex; let mut temp_state = ctx.state.with_temp_write_log(); - let mut tx = Tx::try_from(&request.data[..]).into_storage_result()?; + let tx = Tx::try_from(&request.data[..]).into_storage_result()?; tx.validate_tx().into_storage_result()?; let mut cumulated_gas = Gas::default(); @@ -77,12 +77,10 @@ mod dry_run_tx { temp_state.write_log_mut().commit_tx(); cumulated_gas = tx_gas_meter.borrow_mut().get_tx_consumed_gas(); - - tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); let available_gas = tx_gas_meter.borrow().get_available_gas(); TxGasMeter::new_from_sub_limit(available_gas) } - TxType::Protocol(_) | TxType::Decrypted(_) => { + TxType::Protocol(_) => { // If dry run only the inner tx, use the max block gas as // the gas limit TxGasMeter::new(GasLimit::from( @@ -91,7 +89,6 @@ mod dry_run_tx { } TxType::Raw => { // Cast tx to a decrypted for execution - tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted)); // If dry run only the inner tx, use the max block gas as // the gas limit @@ -145,7 +142,6 @@ mod test { use namada_state::testing::TestState; use namada_state::StorageWrite; use namada_test_utils::TestWasms; - use namada_tx::data::decrypted::DecryptedTx; use namada_tx::data::TxType; use namada_tx::{Code, Data, Tx}; use tempfile::TempDir; @@ -286,8 +282,7 @@ mod test { assert_eq!(current_epoch, read_epoch); // Request dry run tx - let mut outer_tx = - Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted)); + let mut outer_tx = Tx::from_type(TxType::Raw); outer_tx.header.chain_id = client.state.in_mem().chain_id.clone(); outer_tx.set_code(Code::from_hash(tx_hash, None)); outer_tx.set_data(Data::new(vec![])); diff --git a/crates/namada/src/ledger/native_vp/masp.rs b/crates/namada/src/ledger/native_vp/masp.rs index 67a9c64a7d..71412f294e 100644 --- a/crates/namada/src/ledger/native_vp/masp.rs +++ b/crates/namada/src/ledger/native_vp/masp.rs @@ -12,7 +12,7 @@ use masp_primitives::transaction::Transaction; use namada_core::address::Address; use namada_core::address::InternalAddress::Masp; use namada_core::masp::encode_asset_type; -use namada_core::storage::{IndexedTx, Key}; +use namada_core::storage::{Key, StoredIndexedTx}; use namada_gas::MASP_VERIFY_SHIELDED_TX_GAS; use namada_sdk::masp::verify_shielded_tx; use namada_state::{OptionExt, ResultExt, StateRead}; diff --git a/crates/namada/src/ledger/protocol/mod.rs b/crates/namada/src/ledger/protocol/mod.rs index 724f52208c..adaed2fafd 100644 --- a/crates/namada/src/ledger/protocol/mod.rs +++ b/crates/namada/src/ledger/protocol/mod.rs @@ -209,7 +209,7 @@ where &tx_index, ShellParams { tx_gas_meter, - wl_storage, + state, vp_wasm_cache, tx_wasm_cache, }, @@ -218,7 +218,6 @@ where inner_res.wrapper_changed_keys = changed_keys; Ok(inner_res) } - TxType::Decrypted(_) => Err(Error::TxTypeError), } } @@ -1040,11 +1039,13 @@ mod tests { use borsh::BorshDeserialize; use eyre::Result; - use namada_core::chain::ChainId; + use namada_core::address::testing::nam; use namada_core::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; use namada_core::ethereum_events::{EthereumEvent, TransferToNamada}; use namada_core::keccak::keccak_hash; - use namada_core::storage::BlockHeight; + use namada_core::key::RefTo; + use namada_core::storage::{BlockHeight, Epoch}; + use namada_core::token::DenominatedAmount; use namada_core::voting_power::FractionalVotingPower; use namada_core::{address, key}; use namada_ethereum_bridge::protocol::transactions::votes::{ @@ -1054,6 +1055,7 @@ mod tests { use namada_ethereum_bridge::storage::proof::EthereumProof; use namada_ethereum_bridge::storage::{vote_tallies, vp}; use namada_ethereum_bridge::test_utils; + use namada_tx::data::Fee; use namada_tx::{SignableEthMessage, Signed}; use namada_vote_ext::bridge_pool_roots::BridgePoolRootVext; use namada_vote_ext::ethereum_events::EthereumEventsVext; @@ -1195,10 +1197,7 @@ mod tests { #[test] fn test_apply_wasm_tx_allowlist() { let (mut state, _validators) = test_utils::setup_default_storage(); - - let mut rng: ThreadRng = thread_rng(); - ed25519::SigScheme::generate(&mut rng).try_to_sk().unwrap() - }; + let keypair = key::testing::keypair_1(); let wrapper_tx = WrapperTx::new( Fee { amount_per_gas_unit: DenominatedAmount::native( diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 71dc57aa46..ee49388709 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -965,7 +965,6 @@ pub mod testing { // Generate an arbitrary transaction type pub fn arb_tx_type()(tx_type in prop_oneof![ Just(TxType::Raw), - arb_decrypted_tx().prop_map(TxType::Decrypted), arb_wrapper_tx().prop_map(|x| TxType::Wrapper(Box::new(x))), ]) -> TxType { tx_type diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index c7202ff760..1b846184e6 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -152,9 +152,8 @@ pub enum TransferErr { #[derive(Debug, Clone)] struct ExtractedMaspTx { - fee_unshielding: - Option<(BTreeSet, Transaction)>, - inner_tx: Option<(BTreeSet, Transaction)>, + fee_unshielding: Option<(BTreeSet, Transaction)>, + inner_tx: Option<(BTreeSet, Transaction)>, } /// MASP verifying keys @@ -2332,10 +2331,12 @@ impl ShieldedContext { || IndexedTx { height: BlockHeight::first(), index: TxIndex(0), + is_wrapper: false, }, |indexed| IndexedTx { height: indexed.height, index: indexed.index + 1, + is_wrapper: false, }, ); self.sync_status = ContextSyncStatus::Speculative; diff --git a/crates/state/src/in_memory.rs b/crates/state/src/in_memory.rs index 2d53b92b7c..b00a496c4e 100644 --- a/crates/state/src/in_memory.rs +++ b/crates/state/src/in_memory.rs @@ -7,7 +7,7 @@ use namada_gas::MEMORY_ACCESS_GAS_PER_BYTE; use namada_merkle_tree::{MerkleRoot, MerkleTree}; use namada_parameters::{EpochDuration, Parameters}; use namada_storage::conversion_state::ConversionState; -use namada_storage::tx_queue::{ExpiredTxsQueue, TxQueue}; +use namada_storage::tx_queue::ExpiredTxsQueue; use namada_storage::{ BlockHash, BlockHeight, BlockResults, Epoch, Epochs, EthEventsQueue, Header, Key, KeySeg, StorageHasher, TxIndex, BLOCK_HASH_LENGTH, @@ -54,8 +54,6 @@ where pub tx_index: TxIndex, /// The currently saved conversion state pub conversion_state: ConversionState, - /// Wrapper txs to be decrypted in the next block proposal - pub tx_queue: TxQueue, /// Queue of expired transactions that need to be retransmitted. /// /// These transactions do not need to be persisted, as they are @@ -139,7 +137,6 @@ where update_epoch_blocks_delay: None, tx_index: TxIndex::default(), conversion_state: ConversionState::default(), - tx_queue: TxQueue::default(), expired_txs_queue: ExpiredTxsQueue::default(), native_token, ethereum_height: None, diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index e8aaf8df9d..05d66ab980 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -585,7 +585,7 @@ pub mod testing { use namada_core::address::EstablishedAddressGen; use namada_core::chain::ChainId; use namada_core::time::DateTimeUtc; - use namada_storage::tx_queue::{ExpiredTxsQueue, TxQueue}; + use namada_storage::tx_queue::ExpiredTxsQueue; use super::mockdb::MockDB; use super::*; diff --git a/crates/state/src/wl_state.rs b/crates/state/src/wl_state.rs index c90403ea3c..59ebce8cea 100644 --- a/crates/state/src/wl_state.rs +++ b/crates/state/src/wl_state.rs @@ -13,7 +13,7 @@ use namada_storage::{BlockHeight, BlockStateRead, BlockStateWrite, ResultExt}; use crate::in_memory::InMemory; use crate::write_log::{ - self, ReProtStorageModification, StorageModification, WriteLog, + ReProtStorageModification, StorageModification, WriteLog, }; use crate::{ is_pending_transfer_key, DBIter, Epoch, Error, Hash, Key, LastBlock, @@ -467,7 +467,6 @@ where results, address_gen, conversion_state, - tx_queue, ethereum_height, eth_events_queue, }) = self @@ -500,7 +499,6 @@ where let in_mem = &mut self.0.in_mem; in_mem.block.tree = tree; in_mem.conversion_state = conversion_state; - in_mem.tx_queue = tx_queue; in_mem.ethereum_height = ethereum_height; in_mem.eth_events_queue = eth_events_queue; tracing::debug!("Loaded storage from DB"); @@ -554,7 +552,6 @@ where update_epoch_blocks_delay: self.in_mem.update_epoch_blocks_delay, address_gen: &self.in_mem.address_gen, conversion_state: &self.in_mem.conversion_state, - tx_queue: &self.in_mem.tx_queue, ethereum_height: self.in_mem.ethereum_height.as_ref(), eth_events_queue: &self.in_mem.eth_events_queue, }; @@ -628,8 +625,8 @@ where } /// Delete the provided transaction's hash from storage. - pub fn delete_tx_hash(&mut self, hash: Hash) -> write_log::Result<()> { - self.write_log.delete_tx_hash(hash) + pub fn delete_tx_hash(&mut self, hash: Hash) { + self.write_log.delete_tx_hash(hash); } #[inline] diff --git a/crates/state/src/write_log.rs b/crates/state/src/write_log.rs index ec2597d56c..b3c97d6772 100644 --- a/crates/state/src/write_log.rs +++ b/crates/state/src/write_log.rs @@ -607,24 +607,9 @@ impl WriteLog { } /// Remove the transaction hash - pub fn delete_tx_hash(&mut self, hash: Hash) -> Result<()> { - match self - .replay_protection - .insert(hash, ReProtStorageModification::Delete) - { - None => Ok(()), - // Allow overwriting a previous finalize request - Some(ReProtStorageModification::Finalize) => Ok(()), - Some(_) => - // Cannot delete an hash that still has to be written to - // storage or has already been deleted - { - Err(Error::ReplayProtection(format!( - "Requested a delete on hash {hash} not yet committed to \ - storage" - ))) - } - } + pub(crate) fn delete_tx_hash(&mut self, hash: Hash) { + self.replay_protection + .insert(hash, ReProtStorageModification::Delete); } /// Move the transaction hash of the previous block to the list of all @@ -916,8 +901,7 @@ mod tests { // delete previous hash write_log - .delete_tx_hash(Hash::sha256("tx1".as_bytes())) - .unwrap(); + .delete_tx_hash(Hash::sha256("tx1".as_bytes())); // finalize previous hashes for tx in ["tx2", "tx3"] { @@ -947,8 +931,7 @@ mod tests { // try to delete finalized hash which shouldn't work state .write_log - .delete_tx_hash(Hash::sha256("tx2".as_bytes())) - .unwrap(); + .delete_tx_hash(Hash::sha256("tx2".as_bytes())); // commit a block state.commit_block().expect("commit failed"); diff --git a/crates/storage/src/tx_queue.rs b/crates/storage/src/tx_queue.rs index 3d5d9c7d87..87768dcbbe 100644 --- a/crates/storage/src/tx_queue.rs +++ b/crates/storage/src/tx_queue.rs @@ -1,52 +1,5 @@ use namada_core::borsh::{BorshDeserialize, BorshSerialize}; use namada_core::ethereum_events::EthereumEvent; -use namada_gas::Gas; -use namada_tx::Tx; - -/// A wrapper for `crate::types::transaction::WrapperTx` to conditionally -/// add `has_valid_pow` flag for only used in testnets. -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] -pub struct TxInQueue { - /// Wrapper tx - pub tx: Tx, - /// The available gas remaining for the inner tx (for gas accounting). - /// This allows for a more detailed logging about the gas used by the - /// wrapper and that used by the inner - pub gas: Gas, -} - -#[derive(Default, Debug, Clone, BorshDeserialize, BorshSerialize)] -/// Wrapper txs to be decrypted in the next block proposal -pub struct TxQueue(std::collections::VecDeque); - -impl TxQueue { - /// Add a new wrapper at the back of the queue - pub fn push(&mut self, wrapper: TxInQueue) { - self.0.push_back(wrapper); - } - - /// Remove the wrapper at the head of the queue - pub fn pop(&mut self) -> Option { - self.0.pop_front() - } - - /// Get an iterator over the queue - pub fn iter(&self) -> impl std::iter::Iterator { - self.0.iter() - } - - /// Check if there are any txs in the queue - #[allow(dead_code)] - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Get reference to the element at the given index. - /// Returns [`None`] if index exceeds the queue lenght. - pub fn get(&self, index: usize) -> Option<&TxInQueue> { - self.0.get(index) - } -} /// Expired transaction kinds. #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] diff --git a/crates/tests/src/e2e/ledger_tests.rs b/crates/tests/src/e2e/ledger_tests.rs index 2d5fc7a8ee..b36050e71b 100644 --- a/crates/tests/src/e2e/ledger_tests.rs +++ b/crates/tests/src/e2e/ledger_tests.rs @@ -843,7 +843,6 @@ fn wrapper_disposable_signer() -> Result<()> { ]; let mut client = run!(test, Bin::Client, tx_args, Some(720))?; - client.exp_string(TX_ACCEPTED)?; client.exp_string(TX_APPLIED_SUCCESS)?; Ok(()) } diff --git a/crates/tx/src/data/mod.rs b/crates/tx/src/data/mod.rs index db628d11e4..b3fa78553b 100644 --- a/crates/tx/src/data/mod.rs +++ b/crates/tx/src/data/mod.rs @@ -301,8 +301,6 @@ pub enum TxType { Raw, /// A Tx that contains an encrypted raw tx Wrapper(Box), - /// An attempted decryption of a wrapper tx - Decrypted(DecryptedTx), /// Txs issued by validators as part of internal protocols Protocol(Box), } @@ -479,74 +477,3 @@ mod test_process_tx { assert_matches!(result, TxError::SigError(_)); } } - -/// Test that process_tx correctly identifies a DecryptedTx -/// with some unsigned data and returns an identical copy -#[test] -fn test_process_tx_decrypted_unsigned() { - use crate::{Code, Data, Tx}; - let mut tx = Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted)); - let code_sec = tx - .set_code(Code::new("transaction data".as_bytes().to_owned(), None)) - .clone(); - let data_sec = tx - .set_data(Data::new("transaction data".as_bytes().to_owned())) - .clone(); - tx.validate_tx().expect("Test failed"); - match tx.header().tx_type { - TxType::Decrypted(DecryptedTx::Decrypted) => { - assert_eq!(tx.header().code_hash, code_sec.get_hash(),); - assert_eq!(tx.header().data_hash, data_sec.get_hash(),); - } - _ => panic!("Test failed"), - } -} - -/// Test that process_tx correctly identifies a DecryptedTx -/// with some signed data and extracts it without checking -/// signature -#[test] -fn test_process_tx_decrypted_signed() { - use namada_core::key::*; - - use crate::{Code, Data, Section, Signature, Tx}; - - fn gen_keypair() -> common::SecretKey { - use rand::prelude::ThreadRng; - use rand::thread_rng; - - let mut rng: ThreadRng = thread_rng(); - ed25519::SigScheme::generate(&mut rng).try_to_sk().unwrap() - } - - use namada_core::key::Signature as S; - let mut decrypted = - Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted)); - // Invalid signed data - let ed_sig = - ed25519::Signature::try_from_slice([0u8; 64].as_ref()).unwrap(); - let mut sig_sec = Signature::new( - vec![decrypted.header_hash()], - [(0, gen_keypair())].into_iter().collect(), - None, - ); - sig_sec - .signatures - .insert(0, common::Signature::try_from_sig(&ed_sig).unwrap()); - decrypted.add_section(Section::Signature(sig_sec)); - // create the tx with signed decrypted data - let code_sec = decrypted - .set_code(Code::new("transaction data".as_bytes().to_owned(), None)) - .clone(); - let data_sec = decrypted - .set_data(Data::new("transaction data".as_bytes().to_owned())) - .clone(); - decrypted.validate_tx().expect("Test failed"); - match decrypted.header().tx_type { - TxType::Decrypted(DecryptedTx::Decrypted) => { - assert_eq!(decrypted.header.code_hash, code_sec.get_hash()); - assert_eq!(decrypted.header.data_hash, data_sec.get_hash()); - } - _ => panic!("Test failed"), - } -} diff --git a/crates/tx/src/data/wrapper.rs b/crates/tx/src/data/wrapper.rs index 705a88ec34..eb9ecaa3f2 100644 --- a/crates/tx/src/data/wrapper.rs +++ b/crates/tx/src/data/wrapper.rs @@ -22,7 +22,7 @@ pub mod wrapper_tx { use sha2::{Digest, Sha256}; use thiserror::Error; - use crate::data::{DecryptedTx, TxType}; + use crate::data::TxType; use crate::{Code, Data, Section, Tx}; /// TODO: Determine a sane number for this @@ -297,8 +297,7 @@ pub mod wrapper_tx { transfer_code_tag: Option, unshield: Transaction, ) -> Result { - let mut tx = - Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted)); + let mut tx = Tx::from_type(TxType::Raw); let masp_section = tx.add_section(Section::MaspTx(unshield)); let masp_hash = Hash( masp_section diff --git a/crates/tx/src/lib.rs b/crates/tx/src/lib.rs index 6c866e8513..f67e0c1a5a 100644 --- a/crates/tx/src/lib.rs +++ b/crates/tx/src/lib.rs @@ -21,25 +21,12 @@ pub use types::{ pub fn new_tx_event(tx: &Tx, height: u64) -> Event { let mut event = match tx.header().tx_type { TxType::Wrapper(_) => { - let mut event = Event { - event_type: EventType::Accepted, - level: EventLevel::Tx, - attributes: HashMap::new(), - }; - event["hash"] = tx.header_hash().to_string(); - event - } - TxType::Decrypted(_) => { let mut event = Event { event_type: EventType::Applied, level: EventLevel::Tx, attributes: HashMap::new(), }; - event["hash"] = tx - .clone() - .update_header(TxType::Raw) - .header_hash() - .to_string(); + event["hash"] = tx.header_hash().to_string(); event } TxType::Protocol(_) => { diff --git a/crates/tx/src/types.rs b/crates/tx/src/types.rs index 9533209425..b5b6dab7a8 100644 --- a/crates/tx/src/types.rs +++ b/crates/tx/src/types.rs @@ -27,7 +27,7 @@ use sha2::{Digest, Sha256}; use thiserror::Error; use crate::data::protocol::ProtocolTx; -use crate::data::{hash_tx, DecryptedTx, Fee, GasLimit, TxType, WrapperTx}; +use crate::data::{hash_tx, Fee, GasLimit, TxType, WrapperTx}; use crate::proto; /// Represents an error in signature verification @@ -904,15 +904,6 @@ impl Header { } } - /// Get the decrypted header if it is present - pub fn decrypted(&self) -> Option { - if let TxType::Decrypted(decrypted) = &self.tx_type { - Some(decrypted.clone()) - } else { - None - } - } - /// Get the protocol header if it is present pub fn protocol(&self) -> Option { if let TxType::Protocol(protocol) = &self.tx_type { @@ -1341,8 +1332,6 @@ impl Tx { err )) }), - // we extract the signed data, but don't check the signature - TxType::Decrypted(_) => Ok(None), // return as is TxType::Raw => Ok(None), } From e138cfb7604744a45b8a84fc16a52ae86ec1c73f Mon Sep 17 00:00:00 2001 From: satan Date: Sun, 3 Mar 2024 11:10:14 +0100 Subject: [PATCH 17/17] [fix]: Integration tests --- .../lib/node/ledger/shell/finalize_block.rs | 3 +-- crates/core/src/storage.rs | 22 ------------------- crates/namada/src/ledger/native_vp/masp.rs | 6 ++--- crates/state/src/write_log.rs | 3 +-- 4 files changed, 5 insertions(+), 29 deletions(-) diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index e1ba3e73ce..38cb8fbf90 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -627,8 +627,7 @@ where .write_tx_hash(raw_header_hash) .expect("Error while writing tx hash to storage"); - self.state - .delete_tx_hash(header_hash) + self.state.delete_tx_hash(header_hash) } } } diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index ada97688bc..58bfbc9124 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -1473,28 +1473,6 @@ pub struct IndexedTx { pub is_wrapper: bool, } -#[derive( - Default, - Debug, - Copy, - Clone, - BorshSerialize, - BorshDeserialize, - Eq, - PartialEq, - Ord, - PartialOrd, -)] -/// The index of a tx to be stored in the DB. Differs from -/// [`IndexedTx`] as we know we only persist txs which aren't -/// wrapper txs. -pub struct StoredIndexedTx { - /// The block height of the indexed tx - pub height: BlockHeight, - /// The index in the block of the tx - pub index: TxIndex, -} - #[cfg(test)] /// Tests and strategies for storage pub mod tests { diff --git a/crates/namada/src/ledger/native_vp/masp.rs b/crates/namada/src/ledger/native_vp/masp.rs index 71412f294e..2eb6f7e117 100644 --- a/crates/namada/src/ledger/native_vp/masp.rs +++ b/crates/namada/src/ledger/native_vp/masp.rs @@ -12,7 +12,7 @@ use masp_primitives::transaction::Transaction; use namada_core::address::Address; use namada_core::address::InternalAddress::Masp; use namada_core::masp::encode_asset_type; -use namada_core::storage::{Key, StoredIndexedTx}; +use namada_core::storage::{IndexedTx, Key}; use namada_gas::MASP_VERIFY_SHIELDED_TX_GAS; use namada_sdk::masp::verify_shielded_tx; use namada_state::{OptionExt, ResultExt, StateRead}; @@ -265,9 +265,9 @@ where 1 => { match self .ctx - .read_post::(pin_keys.first().unwrap())? + .read_post::(pin_keys.first().unwrap())? { - Some(StoredIndexedTx { height, index }) + Some(IndexedTx { height, index, .. }) if height == self.ctx.get_block_height()? && index == self.ctx.get_tx_index()? => {} Some(_) => { diff --git a/crates/state/src/write_log.rs b/crates/state/src/write_log.rs index b3c97d6772..6529c57580 100644 --- a/crates/state/src/write_log.rs +++ b/crates/state/src/write_log.rs @@ -900,8 +900,7 @@ mod tests { .unwrap(); // delete previous hash - write_log - .delete_tx_hash(Hash::sha256("tx1".as_bytes())); + write_log.delete_tx_hash(Hash::sha256("tx1".as_bytes())); // finalize previous hashes for tx in ["tx2", "tx3"] {