From dd7c229844cef4071762ab3d075a49162479ad43 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 13 Jun 2025 16:58:07 +0300 Subject: [PATCH] add some test cases --- beacon_node/beacon_chain/src/beacon_chain.rs | 3 +- .../src/inclusion_list_verification.rs | 4 +- .../tests/inclusion_list_verification.rs | 284 ++++++++++++++++++ beacon_node/beacon_chain/tests/main.rs | 1 + .../gossip_methods.rs | 1 - consensus/types/src/chain_spec.rs | 3 +- .../src/inclusion_list_service.rs | 5 +- 7 files changed, 294 insertions(+), 7 deletions(-) create mode 100644 beacon_node/beacon_chain/tests/inclusion_list_verification.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 980f60cbb3..a94b87b400 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2095,6 +2095,7 @@ impl BeaconChain { )?) } + // TODO(focil) rename function /// Produce an `InclusionList` that is valid for the given `slot`. /// /// The produced `InclusionList` will not be valid until it has been signed by exactly one @@ -2146,7 +2147,7 @@ impl BeaconChain { }; let current_slot = self.slot()?; - let next_slot = current_slot.safe_add(1)?; + let _next_slot = current_slot.safe_add(1)?; // Don't bother with the inclusion list if the head is not the current slot. // diff --git a/beacon_node/beacon_chain/src/inclusion_list_verification.rs b/beacon_node/beacon_chain/src/inclusion_list_verification.rs index fe23cdbaba..6c51b8b55b 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_verification.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_verification.rs @@ -22,7 +22,6 @@ pub enum GossipInclusionListError { InvalidSignature, BeaconChainError(Box), PriorInclusionListKnown, - InclusionListSeen, // TODO: equivocation e.g. PriorInclusionListKnown } @@ -90,7 +89,6 @@ impl GossipVerifiedInclusionList { .map_err(|_| GossipInclusionListError::InvalidCommitteeRoot)?; if signed_il.message.inclusion_list_committee_root != il_committee.tree_hash_root() { - tracing::error!("INVALID COMMITTEE ROOT"); return Err(GossipInclusionListError::InvalidCommitteeRoot); } @@ -122,7 +120,7 @@ impl GossipVerifiedInclusionList { } if chain.inclusion_list_seen(&signed_il) { - return Err(GossipInclusionListError::InclusionListSeen); + return Err(GossipInclusionListError::PriorInclusionListKnown); } Ok(Self { diff --git a/beacon_node/beacon_chain/tests/inclusion_list_verification.rs b/beacon_node/beacon_chain/tests/inclusion_list_verification.rs new file mode 100644 index 0000000000..91dffaae91 --- /dev/null +++ b/beacon_node/beacon_chain/tests/inclusion_list_verification.rs @@ -0,0 +1,284 @@ +use std::sync::{Arc, LazyLock}; + +use beacon_chain::{ + inclusion_list_verification::GossipInclusionListError, + test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, + BeaconChainTypes, ChainConfig, +}; +use bls::{generics::GenericSignature, PublicKeyBytes, SecretKey}; +use types::{ + ChainSpec, Domain, Epoch, EthSpec, Fork, Hash256, InclusionList, Keypair, MainnetEthSpec, + SignedInclusionList, SignedRoot, Slot, +}; + +pub const VALIDATOR_COUNT: usize = 256; + +/// A cached set of keys. +static KEYPAIRS: LazyLock> = + LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT)); + +pub type E = MainnetEthSpec; + +/// Returns a beacon chain harness for the eip7805 fork +fn get_harness_eip7805(validator_count: usize) -> BeaconChainHarness> { + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.eip7805_fork_epoch = Some(Epoch::new(0)); + let spec = Arc::new(spec); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + +pub async fn get_valid_signed_inclusion_list( + harness: &BeaconChainHarness, +) -> SignedInclusionList { + let head = harness.chain.head_snapshot(); + let current_epoch = harness.chain.epoch().expect("should get slot"); + let indices = (0..=256).collect::>(); + + let indices_and_pubkeys: Vec<(usize, PublicKeyBytes)> = harness + .chain + .validator_pubkey_bytes_many(&indices) + .unwrap() + .into_iter() + .collect(); + + let inclusion_list_duties = harness + .chain + .validator_inclusion_list_duties( + &indices_and_pubkeys, + current_epoch, + head.beacon_block_root, + ) + .unwrap(); + + let current_slot = MainnetEthSpec::slots_per_epoch() as usize * 3; + + let duties = inclusion_list_duties.0; + + let mut current_duty_for_slot_opt = None; + for duty in duties { + if duty.unwrap().slot == Slot::new(current_slot as u64) { + current_duty_for_slot_opt = duty; + break; + } + } + + let current_duty_for_slot = current_duty_for_slot_opt.unwrap(); + + let inclusion_list = InclusionList { + slot: current_duty_for_slot.slot, + validator_index: current_duty_for_slot.validator_index, + inclusion_list_committee_root: current_duty_for_slot.committee_root, + transactions: <_>::default(), + }; + + let keypair = &KEYPAIRS[current_duty_for_slot.validator_index as usize]; + + let fork = harness.chain.spec.fork_at_epoch(current_epoch); + + let signed_inclusion_list = sign( + &inclusion_list, + &keypair.sk, + &fork, + harness.chain.genesis_validators_root, + &harness.chain.spec, + ); + + signed_inclusion_list +} + +/// Signs `self`, setting the `committee_position`'th bit of `aggregation_bits` to `true`. +/// +/// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`. +pub fn sign( + inclusion_list: &InclusionList, + secret_key: &SecretKey, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &ChainSpec, +) -> SignedInclusionList { + let domain = spec.get_domain( + inclusion_list.slot.epoch(E::slots_per_epoch()), + Domain::InclusionListCommittee, + fork, + genesis_validators_root, + ); + let message_to_sign = inclusion_list.signing_root(domain); + + let signature = secret_key.sign(message_to_sign); + + SignedInclusionList { + message: inclusion_list.clone(), + signature, + } +} + +struct InclusionListGossipTester { + harness: BeaconChainHarness>, + + /* + * Valid inclusion list + */ + inclusion_list: SignedInclusionList, +} + +impl InclusionListGossipTester { + pub async fn new() -> Self { + let harness = get_harness_eip7805(VALIDATOR_COUNT); + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness + .extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Advance into a slot where there have not been blocks or attestations produced. + harness.advance_slot(); + + let inclusion_list = get_valid_signed_inclusion_list(&harness).await; + + Self { + harness, + inclusion_list, + } + } + + pub fn import_valid_inclusion_list(self) -> Self { + self.harness + .chain + .verify_inclusion_list_for_gossip(&self.inclusion_list) + .unwrap(); + + self.harness + .chain + .on_verified_inclusion_list(self.inclusion_list.clone()); + self + } + + pub fn inspect_inclusion_list_err( + self, + desc: &str, + get_inclusion_list: G, + inspect_err: I, + ) -> Self + where + G: Fn(&Self, &mut SignedInclusionList), + I: Fn(&Self, GossipInclusionListError), + { + let mut il = self.inclusion_list.clone(); + get_inclusion_list(&self, &mut il); + + /* + * Individual verification + */ + let err = self + .harness + .chain + .verify_inclusion_list_for_gossip(&il) + .err() + .unwrap_or_else(|| { + panic!( + "{} should error during verify_inclusion_list_for_gossip", + desc + ) + }); + + inspect_err(&self, err); + self + } +} + +/// Tests verification of `SignedInclusionList` from the gossip network. +#[tokio::test] +async fn inclusion_list_verification() { + InclusionListGossipTester::new() + .await + .inspect_inclusion_list_err( + "inclusion list from past slot", + |_, il| { + il.message.slot = Slot::new(0); + }, + |_, error| { + assert!(matches!( + error, + GossipInclusionListError::InvalidSlot { .. } + )) + }, + ) + .inspect_inclusion_list_err( + "inclusion list with invalid committee root", + |_, il| { + il.message.inclusion_list_committee_root = Hash256::default(); + }, + |_, error| { + assert!(matches!( + error, + GossipInclusionListError::InvalidCommitteeRoot + )) + }, + ) + .inspect_inclusion_list_err( + "inclusion list with invalid signature", + |_, il| { + il.signature = GenericSignature::empty(); + }, + |_, error| assert!(matches!(error, GossipInclusionListError::InvalidSignature)), + ) + .inspect_inclusion_list_err( + "inclusion list with a validator index that doesn't belong in the committee", + |_, il| { + il.message.validator_index = 9999; + }, + |_, error| { + assert!(matches!( + error, + GossipInclusionListError::ValidatorNotInCommittee + )) + }, + ) + .inspect_inclusion_list_err( + "inclusion list with too many transactions", + |_, il| { + il.message.transactions = vec![vec![0u8; 5].into(); 8193].into(); + }, + |_, error| { + assert!(matches!( + error, + GossipInclusionListError::TooManyTransactions + )) + }, + ) + // verify and import the valid inclusion list + .import_valid_inclusion_list() + .inspect_inclusion_list_err( + "inclusion list has already been seen over gossip", + |_, _| {}, + |_, error| { + assert!(matches!( + error, + GossipInclusionListError::PriorInclusionListKnown + )) + }, + ); +} diff --git a/beacon_node/beacon_chain/tests/main.rs b/beacon_node/beacon_chain/tests/main.rs index 942ce81684..c1a0414581 100644 --- a/beacon_node/beacon_chain/tests/main.rs +++ b/beacon_node/beacon_chain/tests/main.rs @@ -4,6 +4,7 @@ mod bellatrix; mod block_verification; mod capella; mod events; +mod inclusion_list_verification; mod op_verification; mod payload_invalidation; mod rewards; diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 2e6d3483dd..89384bc39e 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -2239,7 +2239,6 @@ impl NetworkBeaconProcessor { | GossipInclusionListError::ValidatorNotInCommittee | GossipInclusionListError::TooManyTransactions | GossipInclusionListError::InvalidSignature - | GossipInclusionListError::InclusionListSeen | GossipInclusionListError::PriorInclusionListKnown => { debug!( error = ?err, diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index e0cdd61111..c36a9209a0 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -1400,7 +1400,8 @@ impl ChainSpec { impl Default for ChainSpec { fn default() -> Self { - Self::mainnet() } + Self::mainnet() + } } /// Exact implementation of the *config* object from the Ethereum spec (YAML/JSON). diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index b064c584b9..08423fbe38 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -361,7 +361,10 @@ impl InclusionListService info!( il_count = inclusion_lists.len(), - tx_count = inclusion_lists.iter().map(|i| i.message.transactions.len()).sum::(), + tx_count = inclusion_lists + .iter() + .map(|i| i.message.transactions.len()) + .sum::(), ?validator_indices, ?slot, "Successfully published inclusion lists",