diff --git a/crates/blockchain/state_transition/src/beacon/fork_choice.rs b/crates/blockchain/state_transition/src/beacon/fork_choice.rs index f61bfa5ac..0b3f4d86c 100644 --- a/crates/blockchain/state_transition/src/beacon/fork_choice.rs +++ b/crates/blockchain/state_transition/src/beacon/fork_choice.rs @@ -5639,7 +5639,7 @@ pub fn on_attester_slashing(store: &mut Store, attester_slashing: &AttesterSlash } #[cfg(test)] -mod tests { +pub(crate) mod tests { use ethlambda_storage::backend::InMemoryBackend; use super::*; @@ -5653,7 +5653,7 @@ mod tests { /// A store backed by a fresh in-memory backend, with every checkpoint at /// its default (genesis) value and no anchor block or state written. /// Tests populate only what the function under test actually reads. - fn empty_store() -> Store { + pub(crate) fn empty_store() -> Store { store_anchored_at(Root::ZERO) } @@ -5688,7 +5688,7 @@ mod tests { /// A signed block with an empty body and a zero signature, for tests that /// only care about `slot` and `parent_root`. Phase0-shaped since nothing /// under test here reads anything fork-specific. - fn block(slot: Slot, parent_root: Root) -> SignedBeaconBlock { + pub(crate) fn block(slot: Slot, parent_root: Root) -> SignedBeaconBlock { SignedBeaconBlock::Phase0(phase0::SignedBeaconBlock { message: phase0::BeaconBlock { slot, diff --git a/crates/blockchain/state_transition/src/beacon/inclusion_list.rs b/crates/blockchain/state_transition/src/beacon/inclusion_list.rs index 4e2ec9246..f64d9c040 100644 --- a/crates/blockchain/state_transition/src/beacon/inclusion_list.rs +++ b/crates/blockchain/state_transition/src/beacon/inclusion_list.rs @@ -121,9 +121,31 @@ pub fn transactions_size(inclusion_list: &InclusionList) -> u64 { /// head is `head_root`: `get_shuffling_dependent_root(store, head_root, /// epoch(slot))`. What a committee member signs over, and the key a proposer /// or builder reads the previous slot's lists under. +/// +/// The walk continues past the live-chain index through the stored blocks. +/// The index starts at the finalized block, while the lists of an epoch's last +/// slot depend on the block just below it once the previous epoch finalizes at +/// the boundary, so the index walk alone answered with the finalized block and +/// every epoch's first slot read no lists. Stored blocks are never pruned, so +/// they can finish the walk. A step onto a root the store does not hold (a +/// checkpoint anchor's parent) ends it there, which is the true ancestor when +/// the dependent slot is the one just below the anchor. pub fn inclusion_list_dependent_root(store: &Store, head_root: Root, slot: Slot) -> Root { + let epoch = compute_epoch_at_slot(slot); + let dependent_slot = compute_shuffling_dependent_slot(epoch); let index = store.block_index(); - get_shuffling_dependent_root(&index, head_root, compute_epoch_at_slot(slot)) + let mut root = get_shuffling_dependent_root(&index, head_root, epoch); + loop { + // The index first: `block_entry` decodes the whole block. + let entry = index + .get(&root) + .copied() + .or_else(|| store.block_entry(&root)); + match entry { + Some((block_slot, parent_root)) if block_slot > dependent_slot => root = parent_root, + _ => return root, + } + } } /// A state that answers `get_inclusion_list_committee` for a list at `slot` @@ -295,3 +317,72 @@ pub fn payload_inclusion_list_transactions( .transactions(slot, dependent_root, true), ) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::beacon::fork_choice::tests::{block, empty_store}; + + /// The slot finality prunes the live chain to, and the last slot of the + /// epoch two past it, whose dependent slot is the one just below it. + const FINALIZED: Slot = 128; + const LAST_SLOT: Slot = 191; + + /// A store holding one block per `slots` entry, each the parent of the + /// next, written the way fork choice writes them (`BlockHeaders` and + /// `LiveChain` both get a row). Returns the roots in the order given. + fn chain_store(slots: &[Slot]) -> (Store, Vec) { + let mut store = empty_store(); + let mut parent = Root::ZERO; + let mut roots = Vec::new(); + for (position, &slot) in slots.iter().enumerate() { + let signed = block(slot, parent); + let root = Root::repeat_byte(position as u8 + 1); + store + .insert_signed_block(root, signed) + .expect("insert block"); + roots.push(root); + parent = root; + } + (store, roots) + } + + #[test] + fn the_dependent_root_below_a_pruned_live_chain_is_the_real_ancestor() { + let (mut store, roots) = chain_store(&[0, 100, 127, 128, 150, 191]); + assert_eq!( + compute_shuffling_dependent_slot(compute_epoch_at_slot(LAST_SLOT)), + FINALIZED - 1 + ); + store.prune_live_chain(FINALIZED).expect("prune"); + + let dependent = inclusion_list_dependent_root(&store, roots[5], LAST_SLOT); + + assert_eq!(dependent, roots[2], "the block at the dependent slot"); + } + + #[test] + fn a_skipped_dependent_slot_resolves_to_the_block_before_it_past_the_prune() { + let (mut store, roots) = chain_store(&[0, 100, 120, 128, 150, 191]); + store.prune_live_chain(FINALIZED).expect("prune"); + + let dependent = inclusion_list_dependent_root(&store, roots[5], LAST_SLOT); + + assert_eq!(dependent, roots[2], "the latest block at or below the slot"); + } + + #[test] + fn an_unpruned_live_chain_answers_as_the_index_walk_does() { + let (store, roots) = chain_store(&[0, 100, 127, 128, 150, 191]); + let index = store.block_index(); + let epoch = compute_epoch_at_slot(LAST_SLOT); + + let dependent = inclusion_list_dependent_root(&store, roots[5], LAST_SLOT); + + assert_eq!(dependent, roots[2]); + assert_eq!( + dependent, + get_shuffling_dependent_root(&index, roots[5], epoch) + ); + } +}