diff --git a/src/payload/checkpoint.rs b/src/payload/checkpoint.rs index e73278b..9492d87 100644 --- a/src/payload/checkpoint.rs +++ b/src/payload/checkpoint.rs @@ -1,3 +1,133 @@ +//! # Checkpoint Chain +//! +//! This module implements a chain of **checkpoints** used during payload +//! building. Each checkpoint represents the state *after* applying one mutation +//! (a transaction, a bundle, or a noop barrier), and cheap to clone, move, or +//! drop. +//! +//! The design supports two types of checkpoints: +//! +//! - **Light checkpoints** — store only their local state diff (`BundleState`). +//! - **Fat checkpoints** — additionally store an *accumulated state*, which is +//! a fully squashed view of all diffs up to that point. +//! +//! Fat checkpoints act as **skip-list anchors**, speeding up state lookups and +//! reducing the cost of traversing the checkpoint chain. +//! +//! ## Checkpoint Chain Structure +//! +//! Each checkpoint internally stores: +//! +//! - `prev`: the previous checkpoint in the linear history, +//! - `mutation`: either a barrier or the execution result of a tx/bundle, +//! - `fat_ancestor`: an optional link to the *closest previous* fat checkpoint, +//! - `accumulated_state`: `None` for light checkpoints, `Some` for fat ones. +//! +//! The chain always forms a backward-linked list: +//! +//! ```text +//! base_state +//! | +//! v +//! [C1] <- [C2] <- [C3] <- [C4] <- [C5] <- [C6] <- ... +//! ``` +//! +//! Fat checkpoints (*) introduce skip-edges +//! +//! ```text +//! [C3]* <-----+ +//! ^ | +//! | | +//! [C6]* ------+ +//! ^ +//! | +//! [C8] +//! ``` +//! +//! allowing fast traversal backward through accumulated state windows. +//! +//! ## How Accumulated State Is Built +//! +//! A checkpoint becomes *fat* when `.fat()` is invoked on it. The accumulation +//! logic depends on whether a fat ancestor exists. +//! +//! ### Case 1: No Fat Ancestor Exists +//! +//! This happens near the beginning of the block, before any fat checkpoint is +//! created. The accumulated state is built by squashing *all* diffs from the +//! start of the chain up to this checkpoint: +//! +//! ```text +//! accumulated = squash([base, state1, state2, state3]) +//! ``` +//! +//! This creates a baseline-accumulated snapshot. +//! +//! ### Case 2: A Fat Ancestor Exists +//! +//! Let the history be: +//! +//! ```text +//! base +//! ├─ C1: state1 +//! ├─ C2: state2 +//! ├─ C3: state3 -> FAT, accumulated = squash([base,1,2,3]) +//! ├─ C4: state4 +//! ├─ C5: state5 +//! ├─ C6: state6 -> FAT +//! ├─ C7: state7 +//! ├─ C8: state8 +//! ``` +//! +//! When C6 becomes fat, we **do not reuse accumulated** state1–state3. +//! Instead, we *start* from C4 and only apply from there as the fat ancestor +//! checkpoint already includes its own diff: +//! +//! ```text +//! base +//! ├─ C1: state1 +//! ├─ C2: state2 +//! ├─ C3: state3 -> FAT, accumulated = squash([base,1,2,3]) +//! ├─ C4: state4 +//! ├─ C5: state5 +//! ├─ C6: state6 -> FAT, accumulated = squash([4,5,6]) +//! ├─ C7: state7 +//! ├─ C8: state8 +//! ``` +//! +//! ## State Lookup Logic +//! +//! Given a checkpoint `C9` (light), state access proceeds through these layers: +//! +//! 1. **Local mutation state** (`state9`) +//! 2. **Previous light checkpoints** (C8 and C7) +//! 3. **Hit a fat checkpoint (C6)** +//! - check only its *accumulated* state (C4–C6) +//! 4. **Jump to `C6.fat_ancestor` -> C3** +//! - Check only accumulated state (C1–C3) +//! 5. **Fall back to base state** +//! +//! This ensures: +//! - Lookups do not scan the entire chain, +//! - Fat checkpoints define "state windows" that are collapsed +//! +//! ## TLDR +//! +//! - Light checkpoints store only their local diff. +//! - Fat checkpoints store a squashed snapshot of all diffs since the previous +//! fat checkpoint. +//! - `fat_ancestor` provides skip-list–style acceleration by linking fat +//! checkpoints together. +//! - State lookup walks for light checkpoint: +//! - local diffs +//! - then local diffs of previous light checkpoints, +//! - at the first fat checkpoint, the fat checkpoint accumulated diffs, +//! - then jumps to earlier fat checkpoints accumulated diffs, +//! - then base. +//! +//! This design supports efficient execution, simulation, and incremental block +//! building + use { super::exec::IntoExecutable, crate::{alloy, prelude::*, reth}, @@ -51,7 +181,7 @@ pub enum Error { /// between them. Each of the diverging checkpoints can be used to build /// alternative versions of the payload. /// -/// - Checkpoints are inexpensive to clone, discard and move around. However, +/// - Checkpoints are inexpensive to clone, discard, and move around. However, /// they are expensive to create, as they require executing transactions /// through the EVM and storing the resulting state changes. /// @@ -101,7 +231,7 @@ impl Checkpoint

{ }) } - /// Returns the block context at the root of the checkpoint. + /// Returns the block context at the base of the checkpoint. pub fn block(&self) -> &BlockContext

{ &self.inner.block } @@ -132,7 +262,18 @@ impl Checkpoint

{ /// The state changes that occurred as a result of executing the /// transaction(s) that created this checkpoint. + /// + /// If this is a "fat" checkpoint with accumulated state, returns the + /// accumulated state (which includes all states changes since last fat + /// checkpoint including this local checkpoint's mutation state). Otherwise, + /// returns just the local mutation's state. pub fn state(&self) -> Option<&BundleState> { + // Return accumulated state if this is a fat checkpoint + if let Some(ref accumulated) = self.inner.accumulated_state { + return Some(accumulated); + } + + // Otherwise return the local mutation's state match self.inner.mutation { Mutation::Barrier => None, Mutation::Executable(ref result) => Some(result.state()), @@ -234,6 +375,60 @@ impl Checkpoint

{ ) -> Result, PayloadBuilderError> { P::build_payload(self.clone(), self.block().base_state()) } + + /// Creates a "fat" checkpoint with accumulated state. + /// + /// This method traverses the checkpoint history to the latest fat ancestor + /// and merges all state changes using `BundleState::extend` to create a + /// single accumulated state. The resulting checkpoint can be used as a + /// skip-list anchor point for efficient state lookups. + /// + /// If this checkpoint already has accumulated state, it returns self + /// unchanged. + #[must_use] + pub fn fat(mut self) -> Self { + // If already a fat checkpoint, return self + if self.inner.accumulated_state.is_some() { + return self; + } + + // Collect/clone state diffs from (latest fat ancestor, self] + let mut states = self + .iter_from_fat_ancestor() + .filter_map(|cp| cp.result().map(|r| r.state().clone())); + + let Some(mut accumulated) = states.next() else { + // no mutations (only barriers): don't make this fat. + // TODO: maybe still return a fat checkpoint with an empty accumulated + // state here? + return self.clone(); + }; + + // Extend with the rest of the diffs (each cloned once above). + for state in states { + accumulated.extend(state); + } + + // Try to update in place if exclusive access to checkpoint inner + if let Some(inner) = Arc::get_mut(&mut self.inner) { + inner.accumulated_state = Some(accumulated); + self + } else { + // Fallback: create a new CheckpointInner + Self { + inner: Arc::new(CheckpointInner { + block: self.inner.block.clone(), + prev: self.inner.prev.clone(), + fat_ancestor: self.inner.fat_ancestor.clone(), + depth: self.inner.depth, + mutation: self.inner.mutation.clone(), + accumulated_state: Some(accumulated), + created_at: self.inner.created_at, + context: self.inner.context.clone(), + }), + } + } + } } /// Internal API @@ -246,12 +441,20 @@ impl Checkpoint

{ mutation: Mutation

, context: P::CheckpointContext, ) -> Self { + let fat_ancestor = if self.inner.accumulated_state.is_some() { + Some(Arc::clone(&self.inner)) + } else { + self.inner.fat_ancestor.clone() + }; + Self { inner: Arc::new(CheckpointInner { block: self.inner.block.clone(), prev: Some(Arc::clone(&self.inner)), + fat_ancestor, depth: self.inner.depth + 1, mutation, + accumulated_state: None, created_at: Instant::now(), context, }), @@ -267,8 +470,10 @@ impl Checkpoint

{ inner: Arc::new(CheckpointInner { block, prev: None, + fat_ancestor: None, depth: 0, mutation: Mutation::Barrier, + accumulated_state: None, created_at: Instant::now(), context: Default::default(), }), @@ -285,8 +490,10 @@ impl Checkpoint

{ inner: Arc::new(CheckpointInner { block, prev: None, + fat_ancestor: None, depth: 0, mutation: Mutation::Barrier, + accumulated_state: None, created_at: Instant::now(), context, }), @@ -296,9 +503,22 @@ impl Checkpoint

{ /// Lazy iterator over historic checkpoints. /// Note that it is in reverse history order, starting from the latest applied /// checkpoint up to the first one. + #[allow(unused)] fn iter(&self) -> Successors Option> { <&Self as IntoIterator>::into_iter(self) } + + /// Iterator from the latest fat ancestor (or base if none) to self in order + /// of application NOT lazy, see `Self::iter` instead for lazy backward + /// traversal. + fn iter_from_fat_ancestor(&self) -> impl Iterator> { + let mut chain: Vec<_> = self + .into_iter() + .take_while(|cp| cp.inner.accumulated_state.is_none()) + .collect(); + chain.reverse(); // oldest -> newest + chain.into_iter() + } } impl IntoIterator for &Checkpoint

{ @@ -323,7 +543,7 @@ enum Mutation { /// should not be discarded or reordered. An example of this would be placing /// a barrier after applying sequencer transactions to ensure that they do /// not get reordered by pipelines. Another example would be placing a barrier - /// after every committed flashblock, to ensure that any steps in the pipeline + /// after every committed flashblock to ensure that any steps in the pipeline /// do not modify the committed state of the payload in process. /// /// If there are multiple barriers in the history, the last one is considered @@ -347,6 +567,9 @@ struct CheckpointInner { /// The previous checkpoint in this chain of checkpoints, if any. prev: Option>, + /// The latest "fat" checkpoint in this chain of checkpoints, if any. + fat_ancestor: Option>, + /// The number of checkpoints in the chain starting from the beginning of the /// block context. /// @@ -357,6 +580,10 @@ struct CheckpointInner { /// The mutation kind for the checkpoint. mutation: Mutation

, + /// The accumulated state of the checkpoint, only present if the checkpoint + /// is "fat" + accumulated_state: Option, + /// The timestamp when this checkpoint was created. created_at: Instant, @@ -376,13 +603,53 @@ impl From> for Vec> { } } -/// Any checkpoint can be used as a database reference for an EVM instance. -/// The state at a checkpoint is the cumulative aggregate of all state mutations -/// that occurred in the current checkpoint and all its ancestors on top of the -/// base state of the parent block of the block for which the payload is being -/// built. -impl DatabaseRef for Checkpoint

{ - /// The database error type. +impl CheckpointInner

{ + /// Traverse the checkpoint chain in the logical lookup order and apply `f` + /// to each visible `BundleState`. + /// + /// Semantics: + /// - For light checkpoints: visit their local mutation `BundleState` (if + /// any), then go to `prev`. + /// - For fat checkpoints: visit their `accumulated_state`, then jump to + /// `fat_ancestor`. + /// - Stops as soon as `f` returns `Some(_)`. + fn find_in_chain(&self, mut f: F) -> Option + where + F: FnMut(&BundleState) -> Option, + { + let mut current: Option<&CheckpointInner

> = Some(self); + + while let Some(inner) = current { + if let Some(ref accumulated) = inner.accumulated_state { + // Fat checkpoint: check the accumulated state only, then jump to + // fat_ancestor. + if let Some(found) = f(accumulated) { + return Some(found); + } + + current = inner.fat_ancestor.as_deref(); + } else { + // Light checkpoint: check the local mutation state only, then go to + // prev. + if let Mutation::Executable(ref result) = inner.mutation { + let state = result.state(); + if let Some(found) = f(state) { + return Some(found); + } + } + + current = inner.prev.as_deref(); + } + } + + None + } +} + +/// `DatabaseRef` implementation for `CheckpointInner`. +/// This is the core implementation that efficiently traverses the checkpoint +/// chain using the skip-list structure (`fat_ancestor`) when available. +impl DatabaseRef for CheckpointInner

{ type Error = ProviderError; /// Gets basic account information. @@ -390,49 +657,33 @@ impl DatabaseRef for Checkpoint

{ &self, address: Address, ) -> Result, Self::Error> { - // we want to probe the history of checkpoints in reverse order, - // starting from the most recent one, to find the first checkpoint - // that has touched the given address. - - if let Some(account) = self.iter().find_map(|checkpoint| { - checkpoint - .result()? - .state() + if let Some(account) = self.find_in_chain(|state| { + state .account(&address) - .and_then(|account| account.info.as_ref()) + .and_then(|a| a.info.as_ref()) .cloned() }) { return Ok(Some(account)); } - // none of the checkpoints priori to this have touched this address, - // now we need to check if the account exists in the base state of the - // block context. - if let Some(acc) = self.block().base_state().basic_account(&address)? { - return Ok(Some(acc.into())); + // Fallback to base state. + if let Some(acc) = self.block.base_state().basic_account(&address)? { + Ok(Some(acc.into())) + } else { + Ok(None) } - - // account does not exist - Ok(None) } /// Gets account code by its hash. fn code_by_hash_ref(&self, code_hash: B256) -> Result { - // we want to probe the history of checkpoints in reverse order, - // starting from the most recent one, to find the first checkpoint - // that has created the code with the given hash. - - if let Some(code) = self - .iter() - .find_map(|checkpoint| checkpoint.result()?.state().bytecode(&code_hash)) - { + if let Some(code) = self.find_in_chain(|state| state.bytecode(&code_hash)) { return Ok(code); } - // check if the code exists in the base state of the block context. + // Fallback to base state bytecode. Ok( self - .block() + .block .base_state() .bytecode_by_hash(&code_hash)? .unwrap_or_default() @@ -446,37 +697,29 @@ impl DatabaseRef for Checkpoint

{ address: Address, index: StorageKey, ) -> Result { - // traverse checkpoint history looking for the first checkpoint that - // has touched the given address. - - if let Some(value) = self.iter().find_map(|checkpoint| { - checkpoint - .result()? - .state() + if let Some(value) = self.find_in_chain(|state| { + state .account(&address) - .and_then(|account| account.storage.get(&index)) + .and_then(|a| a.storage.get(&index)) .map(|slot| slot.present_value) }) { return Ok(value); } - // none of the checkpoints prior to this have touched this address, - // now we need to check if the account exists in the base state of the - // block context. + // Fallback to base state storage. Ok( self - .block() + .block .base_state() .storage(address, index.into())? .unwrap_or_default(), ) } - /// Gets block hash by block number. fn block_hash_ref(&self, number: u64) -> Result { Ok( self - .block() + .block .base_state() .block_hash(number)? .unwrap_or_default(), @@ -484,6 +727,44 @@ impl DatabaseRef for Checkpoint

{ } } +/// Any checkpoint can be used as a database reference for an EVM instance. +/// The state at a checkpoint is the cumulative aggregate of all state mutations +/// that occurred in the current checkpoint and all its ancestors on top of the +/// base state of the parent block of the block for which the payload is being +/// built. +/// See `` +impl DatabaseRef for Checkpoint

{ + /// The database error type. + type Error = ProviderError; + + /// Gets basic account information. + fn basic_ref( + &self, + address: Address, + ) -> Result, Self::Error> { + self.inner.basic_ref(address) + } + + /// Gets account code by its hash. + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + self.inner.code_by_hash_ref(code_hash) + } + + /// Gets storage value of address at index. + fn storage_ref( + &self, + address: Address, + index: StorageKey, + ) -> Result { + self.inner.storage_ref(address, index) + } + + /// Gets block hash by block number. + fn block_hash_ref(&self, number: u64) -> Result { + self.inner.block_hash_ref(number) + } +} + impl Clone for Checkpoint

{ fn clone(&self) -> Self { Self { @@ -572,27 +853,17 @@ mod tests { crate::{ payload::checkpoint::{Checkpoint, IntoExecutable, Mutation}, prelude::*, - reth::primitives::Recovered, - test_utils::{BlockContextMocked, test_bundle, test_tx, test_txs}, + test_utils::{ + BlockContextMocked, + apply_multiple, + test_bundle, + test_tx, + test_txs, + }, }, std::time::Instant, }; - /// Helper test function to apply multiple transactions on a checkpoint - fn apply_multiple( - root: Checkpoint

, - txs: &[Recovered>], - ) -> Vec> { - let mut cur = root; - txs - .iter() - .map(|tx| { - cur = cur.apply(tx.clone()).unwrap(); - cur.clone() - }) - .collect() - } - mod internal { use super::*; #[test] @@ -763,4 +1034,190 @@ mod tests { assert_eq!(built_payload.id(), payload.id()); assert_eq!(built_payload.block(), payload.block()); } + + mod fat_checkpoints { + use { + super::*, + crate::{ + alloy::primitives::{Address, B256}, + reth::revm::{DatabaseRef, primitives::U256}, + }, + std::sync::Arc, + }; + + /// return depths of checkpoints produced by `iter_from_fat_ancestor`. + fn depths(cp: &Checkpoint

) -> Vec { + cp.iter_from_fat_ancestor().map(|c| c.depth()).collect() + } + + #[test] + fn fat_on_first_mutation_accumulates_from_base() { + let block = BlockContext::::mocked(); + let base = block.start(); // depth 0, barrier + + let txs = test_txs::(0, 0, 3); + let checkpoints = apply_multiple(base, &txs); + let c1 = checkpoints[0].clone(); + let c2 = checkpoints[1].clone(); + let c3 = checkpoints[2].clone(); + + // Sanity: all light, no accumulated state, no fat ancestors. + assert!(c1.inner.accumulated_state.is_none()); + assert!(c2.inner.accumulated_state.is_none()); + assert!(c3.inner.accumulated_state.is_none()); + assert!(c1.inner.fat_ancestor.is_none()); + assert!(c2.inner.fat_ancestor.is_none()); + assert!(c3.inner.fat_ancestor.is_none()); + + // Make C3 fat: with no existing fat ancestor, we should accumulate + // the whole window from base to C3. + let c3_fat = c3.clone().fat(); + assert!(c3_fat.inner.accumulated_state.is_some()); + // the first fat checkpoint has no fat_ancestor + assert!(c3_fat.inner.fat_ancestor.is_none()); + + // iter_from_fat_ancestor should cover all diffs from base to C3: + // depths [0, 1, 2, 3] (Base, C1, C2, C3). + let window_depths = depths(&c3); + assert_eq!(window_depths, vec![0, 1, 2, 3]); + + // Public API should still work the same way. + assert_eq!(c3_fat.depth(), c3.depth()); + assert_eq!(c3_fat.prev(), c3.prev()); + assert_eq!(c3_fat.as_transaction(), c3.as_transaction()); + assert!(c3_fat.state().is_some()); + } + + #[test] + fn fat_with_existing_fat_ancestor_accumulates_only_last_window() { + let block = BlockContext::::mocked(); + let base = block.start(); + + // Build 6 checkpoints. + let txs = test_txs::(0, 0, 6); + let checkpoints = apply_multiple(base, &txs[0..3]); + + let c3 = checkpoints[2].clone(); // depth 3 + // First fat checkpoint at C3: accumulates [C1, C2, C3]. + let c3_fat = c3.clone().fat(); + + let checkpoints = apply_multiple(c3_fat.clone(), &txs[3..6]); + let c4 = checkpoints[3 - 3].clone(); // depth 4 + let c5 = checkpoints[4 - 3].clone(); // depth 5 + let c6 = checkpoints[5 - 3].clone(); // depth 6 + + assert!(c3_fat.inner.accumulated_state.is_some()); + assert!(c3_fat.inner.fat_ancestor.is_none()); + + // Sanity: the successors C4/C5/C6 should have C3 as fat_ancestor. + assert!(Arc::ptr_eq( + c4.inner + .fat_ancestor + .as_ref() + .expect("expected fat ancestor on C4"), + &c3_fat.inner + )); + assert!(Arc::ptr_eq( + c5.inner + .fat_ancestor + .as_ref() + .expect("expected fat ancestor on C5"), + &c3_fat.inner + )); + assert!(Arc::ptr_eq( + c6.inner + .fat_ancestor + .as_ref() + .expect("expected fat ancestor on C6"), + &c3_fat.inner + )); + + // Make C6 fat: now we should accumulate only the window [C4, C5, C6]. + let c6_fat = c6.clone().fat(); + assert!(c6_fat.inner.accumulated_state.is_some()); + + // The fat ancestor of C6 must be C3. + assert!(Arc::ptr_eq( + c6_fat + .inner + .fat_ancestor + .as_ref() + .expect("fat ancestor on C6"), + &c3_fat.inner + )); + + // iter_from_fat_ancestor on C6 should start after C3, i.e. depths [4, 5, + // 6]. + let window_depths = depths(&c6); + assert_eq!(window_depths, vec![4, 5, 6]); + } + + #[test] + fn iter_from_fat_ancestor_for_light_descendants_uses_latest_fat() { + let block = BlockContext::::mocked(); + let base = block.start(); + + let txs = test_txs::(0, 0, 10); + let checkpoints = apply_multiple(base, &txs[0..3]); + let c3 = checkpoints[2].clone(); + let c3_fat = c3.clone().fat(); + + let checkpoints = apply_multiple(c3_fat.clone(), &txs[3..6]); + let c6 = checkpoints[5 - 3].clone(); + let c6_fat = c6.clone().fat(); + + let checkpoints = apply_multiple(c6_fat.clone(), &txs[6..10]); + let c8 = checkpoints[7 - 3 - 3].clone(); + + // After C6 becomes fat, later checkpoints should see C6 as their + // latest fat ancestor, so the window for C8 is (C6, C8] -> depths [7, 8]. + let window_depths = depths(&c8); + assert_eq!(window_depths, vec![7, 8]); + + // And for C6 itself, the window is from its fat ancestor C3: + // depths [4, 5, 6]. + let window_depths_c6 = depths(&c6); + assert_eq!(window_depths_c6, vec![4, 5, 6]); + + // For C3 (first fat), the window covers from base: [0, 1, 2, 3]. + let window_depths_c3 = depths(&c3); + assert_eq!(window_depths_c3, vec![0, 1, 2, 3]); + } + + #[test] + fn database_ref_traversal_resolves_state_through_fat_windows() { + let block = BlockContext::::mocked(); + let base = block.start(); + + // Build a moderately deep chain. + let txs = test_txs::(0, 0, 8); + let checkpoints = apply_multiple(base, &txs); + + // Make two fat checkpoints as skip-list anchors. + let _c3_fat = checkpoints[2].clone().fat(); + let _c6_fat = checkpoints[5].clone().fat(); + let latest = checkpoints[7].clone(); // C8 + + // We just want to ensure we get the same via Checkpoint and via + // CheckpointInner. + let addr = Address::random(); + let key = U256::from(0); + + // basic_ref should never error + let from_cp = latest.basic_ref(addr).unwrap(); + let from_inner = latest.inner.basic_ref(addr).unwrap(); + assert_eq!(from_cp.is_some(), from_inner.is_some()); + + // storage_ref should never error as well. + let storage_from_cp = latest.storage_ref(addr, key).unwrap(); + let storage_from_inner = latest.inner.storage_ref(addr, key).unwrap(); + assert_eq!(storage_from_cp, storage_from_inner); + + // code_by_hash_ref should be consistent. + let any_hash = B256::random(); + let code_from_cp = latest.code_by_hash_ref(any_hash).unwrap(); + let code_from_inner = latest.inner.code_by_hash_ref(any_hash).unwrap(); + assert_eq!(code_from_cp, code_from_inner); + } + } } diff --git a/src/test_utils/mod.rs b/src/test_utils/mod.rs index 8e62b5e..4077400 100644 --- a/src/test_utils/mod.rs +++ b/src/test_utils/mod.rs @@ -39,14 +39,7 @@ pub use { OneStep, StringEvent, }, - transactions::{ - invalid_tx, - reverting_tx, - test_bundle, - test_tx, - test_txs, - transfer_tx, - }, + transactions::*, }; #[cfg(feature = "optimism")] diff --git a/src/test_utils/transactions.rs b/src/test_utils/transactions.rs index 28b4c17..2172155 100644 --- a/src/test_utils/transactions.rs +++ b/src/test_utils/transactions.rs @@ -130,3 +130,23 @@ pub fn invalid_tx( let signed_tx: types::Transaction

= signed_tx.into(); signed_tx.with_signer(signer.address()) } + +/// Helper test function to apply multiple transactions on a checkpoint +/// +/// # Panics +/// - if `apply` fails +pub fn apply_multiple( + root: Checkpoint

, + txs: &[Recovered>], +) -> Vec> { + let mut cur = root; + txs + .iter() + .map(|tx| { + cur = cur + .apply(tx.clone()) + .expect("test transaction should not fail"); + cur.clone() + }) + .collect() +}