From 68f1b6c12f6bf7e98282e4f0420b7bc8ecc7b730 Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Sat, 2 Nov 2024 17:26:48 +0900 Subject: [PATCH 1/5] add tests for misbehaviour case Signed-off-by: Jun Kimura --- crates/consensus/src/errors.rs | 4 +- crates/consensus/src/fork/deneb.rs | 138 +-- crates/consensus/src/merkle.rs | 1 + crates/light-client-verifier/Cargo.toml | 3 + .../finality_update_period_6.json | 0 .../finality_update_period_7.json | 0 .../finality_update_period_8.json | 0 .../finality_update_period_9.json | 0 .../data/{ => bellatrix}/initial_state.json | 0 .../light_client_update_period_5.json | 0 .../light_client_update_period_6.json | 0 .../light_client_update_period_7.json | 0 .../light_client_update_period_8.json | 0 .../light_client_update_period_9.json | 0 crates/light-client-verifier/src/consensus.rs | 922 +++++++++++++++--- .../light-client-verifier/src/misbehaviour.rs | 9 +- crates/light-client-verifier/src/updates.rs | 11 + 17 files changed, 862 insertions(+), 226 deletions(-) rename crates/light-client-verifier/data/{ => bellatrix}/finality_update_period_6.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/finality_update_period_7.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/finality_update_period_8.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/finality_update_period_9.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/initial_state.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/light_client_update_period_5.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/light_client_update_period_6.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/light_client_update_period_7.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/light_client_update_period_8.json (100%) rename crates/light-client-verifier/data/{ => bellatrix}/light_client_update_period_9.json (100%) diff --git a/crates/consensus/src/errors.rs b/crates/consensus/src/errors.rs index 4fe07e0..5772ecd 100644 --- a/crates/consensus/src/errors.rs +++ b/crates/consensus/src/errors.rs @@ -39,8 +39,8 @@ pub enum Error { #[derive(Debug, Display)] pub enum MerkleError { - /// invalid merkle branch error: leaf={0:?} branch={1:?} subtree_index={2:?} root={3:?} - InvalidMerkleBranch(H256, Vec, u32, Root), + /// invalid merkle branch error: leaf={0:?} branch={1:?} subtree_index={2:?} expected={3:?} actual={4:?} + InvalidMerkleBranch(H256, Vec, u32, Root, Root), /// too long merkle branch error: depth={0:?} leaf={1:?} branch={2:?} subtree_index={3:?} root={4:?} TooLongMerkleBranchLength(u32, H256, Vec, u32, Root), /// invalid merkle branch length error: depth={0:?} leaf={1:?} branch={2:?} subtree_index={3:?} root={4:?} diff --git a/crates/consensus/src/fork/deneb.rs b/crates/consensus/src/fork/deneb.rs index 319f5a7..afbce0b 100644 --- a/crates/consensus/src/fork/deneb.rs +++ b/crates/consensus/src/fork/deneb.rs @@ -9,7 +9,7 @@ use crate::{ compute::hash_tree_root, errors::Error, internal_prelude::*, - merkle::MerkleTree, + merkle::{get_subtree_index, MerkleTree}, sync_protocol::{SyncAggregate, SyncCommittee}, types::{Address, ByteList, ByteVector, Bytes32, H256, U256, U64}, }; @@ -365,6 +365,74 @@ pub fn gen_execution_payload_field_proof< )) } +pub fn gen_execution_payload_proof< + const MAX_PROPOSER_SLASHINGS: usize, + const MAX_VALIDATORS_PER_COMMITTEE: usize, + const MAX_ATTESTER_SLASHINGS: usize, + const MAX_ATTESTATIONS: usize, + const DEPOSIT_CONTRACT_TREE_DEPTH: usize, + const MAX_DEPOSITS: usize, + const MAX_VOLUNTARY_EXITS: usize, + const BYTES_PER_LOGS_BLOOM: usize, + const MAX_EXTRA_DATA_BYTES: usize, + const MAX_BYTES_PER_TRANSACTION: usize, + const MAX_TRANSACTIONS_PER_PAYLOAD: usize, + const MAX_WITHDRAWALS_PER_PAYLOAD: usize, + const MAX_BLS_TO_EXECUTION_CHANGES: usize, + const SYNC_COMMITTEE_SIZE: usize, + const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize, +>( + body: &BeaconBlockBody< + MAX_PROPOSER_SLASHINGS, + MAX_VALIDATORS_PER_COMMITTEE, + MAX_ATTESTER_SLASHINGS, + MAX_ATTESTATIONS, + DEPOSIT_CONTRACT_TREE_DEPTH, + MAX_DEPOSITS, + MAX_VOLUNTARY_EXITS, + BYTES_PER_LOGS_BLOOM, + MAX_EXTRA_DATA_BYTES, + MAX_BYTES_PER_TRANSACTION, + MAX_TRANSACTIONS_PER_PAYLOAD, + MAX_WITHDRAWALS_PER_PAYLOAD, + MAX_BLS_TO_EXECUTION_CHANGES, + SYNC_COMMITTEE_SIZE, + MAX_BLOB_COMMITMENTS_PER_BLOCK, + >, +) -> Result<(Root, Vec), Error> { + let tree = MerkleTree::from_leaves( + ([ + hash_tree_root(body.randao_reveal.clone()).unwrap().0, + hash_tree_root(body.eth1_data.clone()).unwrap().0, + body.graffiti.0, + hash_tree_root(body.proposer_slashings.clone()).unwrap().0, + hash_tree_root(body.attester_slashings.clone()).unwrap().0, + hash_tree_root(body.attestations.clone()).unwrap().0, + hash_tree_root(body.deposits.clone()).unwrap().0, + hash_tree_root(body.voluntary_exits.clone()).unwrap().0, + hash_tree_root(body.sync_aggregate.clone()).unwrap().0, + hash_tree_root(body.execution_payload.clone()).unwrap().0, + hash_tree_root(body.bls_to_execution_changes.clone()) + .unwrap() + .0, + hash_tree_root(body.blob_kzg_commitments.clone()).unwrap().0, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ] as [_; 16]) + .as_ref(), + ); + Ok(( + H256(tree.root().unwrap()), + tree.proof(&[get_subtree_index(DENEB_FORK_SPEC.execution_payload_gindex) as usize]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect(), + )) +} + #[cfg(test)] mod tests { use super::*; @@ -442,72 +510,4 @@ mod tests { .is_ok()); } } - - fn gen_execution_payload_proof< - const MAX_PROPOSER_SLASHINGS: usize, - const MAX_VALIDATORS_PER_COMMITTEE: usize, - const MAX_ATTESTER_SLASHINGS: usize, - const MAX_ATTESTATIONS: usize, - const DEPOSIT_CONTRACT_TREE_DEPTH: usize, - const MAX_DEPOSITS: usize, - const MAX_VOLUNTARY_EXITS: usize, - const BYTES_PER_LOGS_BLOOM: usize, - const MAX_EXTRA_DATA_BYTES: usize, - const MAX_BYTES_PER_TRANSACTION: usize, - const MAX_TRANSACTIONS_PER_PAYLOAD: usize, - const MAX_WITHDRAWALS_PER_PAYLOAD: usize, - const MAX_BLS_TO_EXECUTION_CHANGES: usize, - const SYNC_COMMITTEE_SIZE: usize, - const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize, - >( - body: &BeaconBlockBody< - MAX_PROPOSER_SLASHINGS, - MAX_VALIDATORS_PER_COMMITTEE, - MAX_ATTESTER_SLASHINGS, - MAX_ATTESTATIONS, - DEPOSIT_CONTRACT_TREE_DEPTH, - MAX_DEPOSITS, - MAX_VOLUNTARY_EXITS, - BYTES_PER_LOGS_BLOOM, - MAX_EXTRA_DATA_BYTES, - MAX_BYTES_PER_TRANSACTION, - MAX_TRANSACTIONS_PER_PAYLOAD, - MAX_WITHDRAWALS_PER_PAYLOAD, - MAX_BLS_TO_EXECUTION_CHANGES, - SYNC_COMMITTEE_SIZE, - MAX_BLOB_COMMITMENTS_PER_BLOCK, - >, - ) -> Result<(Root, Vec), Error> { - let tree = MerkleTree::from_leaves( - ([ - hash_tree_root(body.randao_reveal.clone()).unwrap().0, - hash_tree_root(body.eth1_data.clone()).unwrap().0, - body.graffiti.0, - hash_tree_root(body.proposer_slashings.clone()).unwrap().0, - hash_tree_root(body.attester_slashings.clone()).unwrap().0, - hash_tree_root(body.attestations.clone()).unwrap().0, - hash_tree_root(body.deposits.clone()).unwrap().0, - hash_tree_root(body.voluntary_exits.clone()).unwrap().0, - hash_tree_root(body.sync_aggregate.clone()).unwrap().0, - hash_tree_root(body.execution_payload.clone()).unwrap().0, - hash_tree_root(body.bls_to_execution_changes.clone()) - .unwrap() - .0, - hash_tree_root(body.blob_kzg_commitments.clone()).unwrap().0, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ] as [_; 16]) - .as_ref(), - ); - Ok(( - H256(tree.root().unwrap()), - tree.proof(&[get_subtree_index(DENEB_FORK_SPEC.execution_payload_gindex) as usize]) - .proof_hashes() - .iter() - .map(|h| H256::from_slice(h)) - .collect(), - )) - } } diff --git a/crates/consensus/src/merkle.rs b/crates/consensus/src/merkle.rs index 7e34fa7..49ff6d3 100644 --- a/crates/consensus/src/merkle.rs +++ b/crates/consensus/src/merkle.rs @@ -62,6 +62,7 @@ pub fn is_valid_merkle_branch( branch.to_vec(), subtree_index, root, + value, )) } } diff --git a/crates/light-client-verifier/Cargo.toml b/crates/light-client-verifier/Cargo.toml index 9b1f662..93965a6 100644 --- a/crates/light-client-verifier/Cargo.toml +++ b/crates/light-client-verifier/Cargo.toml @@ -16,6 +16,9 @@ rlp = { version = "0.5.2", default-features = false } [dev-dependencies] serde_json = "1.0.91" hex-literal = "0.3.4" +rand = { version = "0.8.5", features = ["std", "std_rng"] } +ssz-rs = { git = "https://github.com/bluele/ssz_rs", branch = "serde-no-std", default-features = false, features = ["serde"] } +milagro_bls = { git = "https://github.com/datachainlab/milagro_bls", rev = "bc2b5b5e8d48b7e2e1bfaa56dc2d93e13cb32095", default-features = false } [features] default = ["std"] diff --git a/crates/light-client-verifier/data/finality_update_period_6.json b/crates/light-client-verifier/data/bellatrix/finality_update_period_6.json similarity index 100% rename from crates/light-client-verifier/data/finality_update_period_6.json rename to crates/light-client-verifier/data/bellatrix/finality_update_period_6.json diff --git a/crates/light-client-verifier/data/finality_update_period_7.json b/crates/light-client-verifier/data/bellatrix/finality_update_period_7.json similarity index 100% rename from crates/light-client-verifier/data/finality_update_period_7.json rename to crates/light-client-verifier/data/bellatrix/finality_update_period_7.json diff --git a/crates/light-client-verifier/data/finality_update_period_8.json b/crates/light-client-verifier/data/bellatrix/finality_update_period_8.json similarity index 100% rename from crates/light-client-verifier/data/finality_update_period_8.json rename to crates/light-client-verifier/data/bellatrix/finality_update_period_8.json diff --git a/crates/light-client-verifier/data/finality_update_period_9.json b/crates/light-client-verifier/data/bellatrix/finality_update_period_9.json similarity index 100% rename from crates/light-client-verifier/data/finality_update_period_9.json rename to crates/light-client-verifier/data/bellatrix/finality_update_period_9.json diff --git a/crates/light-client-verifier/data/initial_state.json b/crates/light-client-verifier/data/bellatrix/initial_state.json similarity index 100% rename from crates/light-client-verifier/data/initial_state.json rename to crates/light-client-verifier/data/bellatrix/initial_state.json diff --git a/crates/light-client-verifier/data/light_client_update_period_5.json b/crates/light-client-verifier/data/bellatrix/light_client_update_period_5.json similarity index 100% rename from crates/light-client-verifier/data/light_client_update_period_5.json rename to crates/light-client-verifier/data/bellatrix/light_client_update_period_5.json diff --git a/crates/light-client-verifier/data/light_client_update_period_6.json b/crates/light-client-verifier/data/bellatrix/light_client_update_period_6.json similarity index 100% rename from crates/light-client-verifier/data/light_client_update_period_6.json rename to crates/light-client-verifier/data/bellatrix/light_client_update_period_6.json diff --git a/crates/light-client-verifier/data/light_client_update_period_7.json b/crates/light-client-verifier/data/bellatrix/light_client_update_period_7.json similarity index 100% rename from crates/light-client-verifier/data/light_client_update_period_7.json rename to crates/light-client-verifier/data/bellatrix/light_client_update_period_7.json diff --git a/crates/light-client-verifier/data/light_client_update_period_8.json b/crates/light-client-verifier/data/bellatrix/light_client_update_period_8.json similarity index 100% rename from crates/light-client-verifier/data/light_client_update_period_8.json rename to crates/light-client-verifier/data/bellatrix/light_client_update_period_8.json diff --git a/crates/light-client-verifier/data/light_client_update_period_9.json b/crates/light-client-verifier/data/bellatrix/light_client_update_period_9.json similarity index 100% rename from crates/light-client-verifier/data/light_client_update_period_9.json rename to crates/light-client-verifier/data/bellatrix/light_client_update_period_9.json diff --git a/crates/light-client-verifier/src/consensus.rs b/crates/light-client-verifier/src/consensus.rs index 9eeaacf..aca8af2 100644 --- a/crates/light-client-verifier/src/consensus.rs +++ b/crates/light-client-verifier/src/consensus.rs @@ -66,10 +66,6 @@ impl Result<(), Error> { - consensus_update.validate_basic(ctx)?; - execution_update.validate_basic()?; - - self.ensure_relevant_update(ctx, store, consensus_update)?; self.validate_consensus_update(ctx, store, consensus_update)?; self.validate_execution_update( ctx.compute_fork_spec(consensus_update.finalized_beacon_header().slot), @@ -88,11 +84,13 @@ impl Result<(), Error> { - let sync_committee = self.get_sync_committee(ctx, store, update)?; - validate_light_client_update(ctx, store, update)?; - verify_sync_committee_attestation(ctx, update, &sync_committee)?; + consensus_update.validate_basic(ctx)?; + self.ensure_relevant_update(ctx, store, consensus_update)?; + let sync_committee = self.get_sync_committee(ctx, store, consensus_update)?; + validate_light_client_update(ctx, store, consensus_update)?; + verify_sync_committee_attestation(ctx, consensus_update, &sync_committee)?; Ok(()) } @@ -101,22 +99,29 @@ impl Result<(), Error> { + execution_update.validate_basic()?; if update_fork_spec.execution_payload_gindex == 0 { return Err(Error::NoExecutionPayloadInBeaconBlock); } is_valid_normalized_merkle_branch( - hash_tree_root(update.state_root()).unwrap().0.into(), - &update.state_root_branch(), + hash_tree_root(execution_update.state_root()) + .unwrap() + .0 + .into(), + &execution_update.state_root_branch(), update_fork_spec.execution_payload_state_root_gindex, trusted_execution_root, ) .map_err(Error::InvalidExecutionStateRootMerkleBranch)?; is_valid_normalized_merkle_branch( - hash_tree_root(update.block_number()).unwrap().0.into(), - &update.block_number_branch(), + hash_tree_root(execution_update.block_number()) + .unwrap() + .0 + .into(), + &execution_update.block_number_branch(), update_fork_spec.execution_payload_block_number_gindex, trusted_execution_root, ) @@ -398,158 +403,773 @@ pub fn verify_bls_signatures( } #[cfg(test)] -mod tests_bellatrix { +mod tests { use super::*; - use crate::{ - context::{Fraction, LightClientContext}, - mock::MockStore, - updates::{ - bellatrix::{ConsensusUpdateInfo, ExecutionUpdateInfo, LightClientBootstrapInfo}, - LightClientBootstrap, - }, - }; - use ethereum_consensus::{ - beacon::Version, - bls::aggreate_public_key, - config::{minimal, Config}, - fork::{ - altair::ALTAIR_FORK_SPEC, bellatrix::BELLATRIX_FORK_SPEC, ForkParameter, ForkParameters, - }, - preset, - types::U64, - }; - use std::{fs, path::PathBuf}; - - const TEST_DATA_DIR: &str = "./data"; - - #[test] - fn test_bootstrap() { - let verifier = SyncProtocolVerifier::< - { preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }, - MockStore<{ preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }>, - >::default(); - let path = format!("{}/initial_state.json", TEST_DATA_DIR); - let (bootstrap, _, genesis_validators_root) = get_init_state(path); - let ctx = LightClientContext::new_with_config( - get_minimal_bellatrix_config(), - genesis_validators_root, - // NOTE: this is workaround. we must get the correct timestamp from beacon state. - minimal::get_config().min_genesis_time, - Fraction::new(2, 3), - 1729846322.into(), - ); - assert!(verifier.validate_boostrap(&ctx, &bootstrap, None).is_ok()); - } - #[test] - fn test_pubkey_aggregation() { - let path = format!("{}/initial_state.json", TEST_DATA_DIR); - let (bootstrap, _, _) = get_init_state(path); - let pubkeys: Vec = bootstrap - .current_sync_committee() - .pubkeys - .iter() - .map(|k| k.clone().try_into().unwrap()) - .collect(); - let aggregated_key = aggreate_public_key(&pubkeys).unwrap(); - let pubkey = BLSPublicKey { - point: aggregated_key.point, + mod bellatrix { + use super::*; + use crate::{ + context::{Fraction, LightClientContext}, + mock::MockStore, + updates::{ + bellatrix::{ConsensusUpdateInfo, ExecutionUpdateInfo, LightClientBootstrapInfo}, + LightClientBootstrap, + }, }; - assert!(pubkey.key_validate()); - - assert!( - pubkey - == bootstrap - .current_sync_committee() - .aggregate_pubkey - .clone() - .try_into() - .unwrap() - ); + use ethereum_consensus::{ + beacon::Version, + bls::aggreate_public_key, + config::{minimal, Config}, + fork::{ + altair::ALTAIR_FORK_SPEC, bellatrix::BELLATRIX_FORK_SPEC, ForkParameter, + ForkParameters, + }, + preset, + types::U64, + }; + use std::{fs, path::PathBuf}; + + const TEST_DATA_DIR: &str = "./data/bellatrix"; + + #[test] + fn test_bootstrap() { + let verifier = SyncProtocolVerifier::< + { preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }, + MockStore<{ preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }>, + >::default(); + let path = format!("{}/initial_state.json", TEST_DATA_DIR); + let (bootstrap, _, genesis_validators_root) = get_init_state(path); + let ctx = LightClientContext::new_with_config( + get_minimal_bellatrix_config(), + genesis_validators_root, + // NOTE: this is workaround. we must get the correct timestamp from beacon state. + minimal::get_config().min_genesis_time, + Fraction::new(2, 3), + 1729846322.into(), + ); + assert!(verifier.validate_boostrap(&ctx, &bootstrap, None).is_ok()); + } + + #[test] + fn test_pubkey_aggregation() { + let path = format!("{}/initial_state.json", TEST_DATA_DIR); + let (bootstrap, _, _) = get_init_state(path); + let pubkeys: Vec = bootstrap + .current_sync_committee() + .pubkeys + .iter() + .map(|k| k.clone().try_into().unwrap()) + .collect(); + let aggregated_key = aggreate_public_key(&pubkeys).unwrap(); + let pubkey = BLSPublicKey { + point: aggregated_key.point, + }; + assert!(pubkey.key_validate()); + + assert!( + pubkey + == bootstrap + .current_sync_committee() + .aggregate_pubkey + .clone() + .try_into() + .unwrap() + ); + } + + #[test] + fn test_verification() { + let verifier = SyncProtocolVerifier::< + { preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }, + MockStore<{ preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }>, + >::default(); + + let (bootstrap, execution_payload_state_root, genesis_validators_root) = + get_init_state(format!("{}/initial_state.json", TEST_DATA_DIR)); + let ctx = LightClientContext::new_with_config( + get_minimal_bellatrix_config(), + genesis_validators_root, + // NOTE: this is workaround. we must get the correct timestamp from beacon state. + minimal::get_config().min_genesis_time, + Fraction::new(2, 3), + 1729846322.into(), + ); + assert!(verifier.validate_boostrap(&ctx, &bootstrap, None).is_ok()); + + let mut store = MockStore::new( + bootstrap.beacon_header().clone(), + bootstrap.current_sync_committee().clone(), + execution_payload_state_root, + ); + + let updates = [ + "light_client_update_period_5.json", + "light_client_update_period_6.json", + "finality_update_period_6.json", + "light_client_update_period_7.json", + "finality_update_period_7.json", + "light_client_update_period_8.json", + "finality_update_period_8.json", + "light_client_update_period_9.json", + "finality_update_period_9.json", + ]; + + for update in updates.into_iter() { + let (consensus_update, execution_update) = + get_updates(format!("{}/{}", TEST_DATA_DIR, update)); + assert!(verifier + .validate_updates(&ctx, &store, &consensus_update, &execution_update) + .is_ok()); + let res = store.apply_light_client_update(&ctx, &consensus_update); + assert!(res.is_ok() && res.unwrap()); + } + } + + // returns boostrap, execution_state_root, genesis_validators_root + fn get_init_state( + path: impl Into, + ) -> ( + LightClientBootstrapInfo<{ preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }>, + H256, + H256, + ) { + let s = fs::read_to_string(path.into()).unwrap(); + serde_json::from_str(&s).unwrap() + } + + fn get_updates( + path: impl Into, + ) -> ( + ConsensusUpdateInfo<{ preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }>, + ExecutionUpdateInfo, + ) { + let s = fs::read_to_string(path.into()).unwrap(); + serde_json::from_str(&s).unwrap() + } + + fn get_minimal_bellatrix_config() -> Config { + Config { + preset: preset::minimal::PRESET, + fork_parameters: ForkParameters::new( + Version([0, 0, 0, 1]), + vec![ + ForkParameter::new(Version([1, 0, 0, 1]), U64(0), ALTAIR_FORK_SPEC), + ForkParameter::new(Version([2, 0, 0, 1]), U64(0), BELLATRIX_FORK_SPEC), + ], + ) + .unwrap(), + min_genesis_time: U64(1578009600), + } + } } - #[test] - fn test_verification() { - let verifier = SyncProtocolVerifier::< - { preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }, - MockStore<{ preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }>, - >::default(); - - let (bootstrap, execution_payload_state_root, genesis_validators_root) = - get_init_state(format!("{}/initial_state.json", TEST_DATA_DIR)); - let ctx = LightClientContext::new_with_config( - get_minimal_bellatrix_config(), - genesis_validators_root, - // NOTE: this is workaround. we must get the correct timestamp from beacon state. - minimal::get_config().min_genesis_time, - Fraction::new(2, 3), - 1729846322.into(), - ); - assert!(verifier.validate_boostrap(&ctx, &bootstrap, None).is_ok()); + mod deneb { + use std::time::SystemTime; - let mut store = MockStore::new( - bootstrap.beacon_header().clone(), - bootstrap.current_sync_committee().clone(), - execution_payload_state_root, - ); + use crate::{ + context::{Fraction, LightClientContext}, + misbehaviour::NextSyncCommitteeMisbehaviour, + mock::MockStore, + }; + use ethereum_consensus::{config, types::U64}; + use utils::{ + gen_light_client_update, gen_light_client_update_with_params, MockSyncCommitteeManager, + }; - let updates = [ - "light_client_update_period_5.json", - "light_client_update_period_6.json", - "finality_update_period_6.json", - "light_client_update_period_7.json", - "finality_update_period_7.json", - "light_client_update_period_8.json", - "finality_update_period_8.json", - "light_client_update_period_9.json", - "finality_update_period_9.json", - ]; - - for update in updates.into_iter() { - let (consensus_update, execution_update) = - get_updates(format!("{}/{}", TEST_DATA_DIR, update)); - assert!(verifier - .validate_updates(&ctx, &store, &consensus_update, &execution_update) + use super::*; + + #[test] + fn test_lc() { + let scm = MockSyncCommitteeManager::<32>::new(1, 4); + let current_sync_committee = scm.get_committee(1); + let ctx = LightClientContext::new_with_config( + config::minimal::get_config(), + Default::default(), + Default::default(), + Fraction::new(2, 3), + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + .into(), + ); + let start_slot = + U64(1) * ctx.slots_per_epoch() * ctx.epochs_per_sync_committee_period(); + + let initial_header = BeaconBlockHeader { + slot: start_slot, + ..Default::default() + }; + let store = MockStore::new( + initial_header, + current_sync_committee.to_committee(), + Default::default(), + ); + + let dummy_execution_state_root = [1u8; 32].into(); + let dummy_execution_block_number = 1; + let update_1 = gen_light_client_update::<32, _>( + &ctx, + start_slot + 11, + start_slot + 10, + U64(1) * ctx.slots_per_epoch(), + dummy_execution_state_root, + dummy_execution_block_number.into(), + &scm, + ); + assert!(SyncProtocolVerifier::default() + .validate_consensus_update(&ctx, &store, &update_1) .is_ok()); - let res = store.apply_light_client_update(&ctx, &consensus_update); - assert!(res.is_ok() && res.unwrap()); + + let update_2_insufficient_attestations = gen_light_client_update_with_params( + &ctx, + start_slot + 11, + start_slot + 10, + U64(1) * ctx.slots_per_epoch(), + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(3), + 21, + ); + assert!(SyncProtocolVerifier::default() + .validate_consensus_update(&ctx, &store, &update_2_insufficient_attestations) + .is_err()); } - } - // returns boostrap, execution_state_root, genesis_validators_root - fn get_init_state( - path: impl Into, - ) -> ( - LightClientBootstrapInfo<{ preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }>, - H256, - H256, - ) { - let s = fs::read_to_string(path.into()).unwrap(); - serde_json::from_str(&s).unwrap() - } + #[test] + fn test_lc_misbehaviour() { + let scm = MockSyncCommitteeManager::<32>::new(1, 4); + let current_sync_committee = scm.get_committee(1); + let ctx = LightClientContext::new_with_config( + config::minimal::get_config(), + Default::default(), + Default::default(), + Fraction::new(2, 3), + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + .into(), + ); + let start_slot = + U64(1) * ctx.slots_per_epoch() * ctx.epochs_per_sync_committee_period(); + + let initial_header = BeaconBlockHeader { + slot: start_slot, + ..Default::default() + }; + let store = MockStore::new( + initial_header, + current_sync_committee.to_committee(), + Default::default(), + ); - fn get_updates( - path: impl Into, - ) -> ( - ConsensusUpdateInfo<{ preset::minimal::PRESET.SYNC_COMMITTEE_SIZE }>, - ExecutionUpdateInfo, - ) { - let s = fs::read_to_string(path.into()).unwrap(); - serde_json::from_str(&s).unwrap() + let dummy_execution_state_root = [1u8; 32].into(); + let dummy_execution_block_number = 1; + let base_signature_slot = start_slot + 11; + let base_attested_slot = start_slot + 10; + let update_1 = gen_light_client_update_with_params::<32, _>( + &ctx, + base_signature_slot, + base_attested_slot, + U64(1) * ctx.slots_per_epoch(), + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(2), + 32, + ); + + { + let update_valid = gen_light_client_update_with_params::<32, _>( + &ctx, + base_signature_slot, + base_attested_slot, + U64(1) * ctx.slots_per_epoch(), + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(3), + 32, + ); + assert!(SyncProtocolVerifier::default() + .validate_misbehaviour( + &ctx, + &store, + Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_valid, + }), + ) + .is_ok()); + } + { + let update_insufficient_attestations = gen_light_client_update_with_params::<32, _>( + &ctx, + base_signature_slot, + base_attested_slot, + U64(1) * ctx.slots_per_epoch(), + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(3), + 21, + ); + assert!(SyncProtocolVerifier::default() + .validate_misbehaviour( + &ctx, + &store, + Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_insufficient_attestations, + },), + ) + .is_err()); + } + { + let update_valid_different_slots = gen_light_client_update_with_params::<32, _>( + &ctx, + base_signature_slot + 1, + base_attested_slot + 1, + U64(1) * ctx.slots_per_epoch(), + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(3), + 32, + ); + assert!(SyncProtocolVerifier::default() + .validate_misbehaviour( + &ctx, + &store, + Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_valid_different_slots, + },), + ) + .is_ok()); + } + {} + } } - fn get_minimal_bellatrix_config() -> Config { - Config { - preset: preset::minimal::PRESET, - fork_parameters: ForkParameters::new( - Version([0, 0, 0, 1]), - vec![ - ForkParameter::new(Version([1, 0, 0, 1]), U64(0), ALTAIR_FORK_SPEC), - ForkParameter::new(Version([2, 0, 0, 1]), U64(0), BELLATRIX_FORK_SPEC), - ], + mod utils { + use crate::updates::{ConsensusUpdateInfo, LightClientUpdate}; + + use super::*; + use ethereum_consensus::{ + beacon::{BlockNumber, Checkpoint, Epoch, Slot}, + bls::{aggreate_public_key, PublicKey, Signature}, + fork::deneb, + merkle::MerkleTree, + preset::mainnet::DenebBeaconBlock, + sync_protocol::SyncAggregate, + types::U64, + }; + use milagro_bls::{ + AggregateSignature, PublicKey as BLSPublicKey, SecretKey as BLSSecretKey, + }; + use ssz_rs::Vector; + + #[derive(Clone)] + struct Validator { + sk: BLSSecretKey, + } + + impl Default for Validator { + fn default() -> Self { + Self { + sk: BLSSecretKey::random(&mut rand::thread_rng()), + } + } + } + + impl Validator { + pub fn sign(&self, msg: H256) -> BLSSignature { + BLSSignature::new(msg.as_bytes(), &self.sk) + } + + pub fn public_key(&self) -> BLSPublicKey { + BLSPublicKey::from_secret_key(&self.sk) + } + } + + #[derive(Clone)] + pub struct MockSyncCommittee { + committee: Vec, + } + + impl MockSyncCommittee { + pub fn new() -> Self { + let mut committee = Vec::new(); + for _ in 0..SYNC_COMMITTEE_SIZE { + committee.push(Validator::default()); + } + Self { committee } + } + + pub fn to_committee(&self) -> SyncCommittee { + let mut pubkeys = Vec::new(); + for v in self.committee.iter() { + pubkeys.push(v.public_key()); + } + let aggregate_pubkey = aggreate_public_key(&pubkeys.to_vec()).unwrap(); + SyncCommittee { + pubkeys: Vector::from_iter(pubkeys.into_iter().map(PublicKey::from)), + aggregate_pubkey: PublicKey::from(aggregate_pubkey), + } + } + + pub fn sign_header( + &self, + ctx: &C, + signature_slot: U64, + attested_header: BeaconBlockHeader, + sign_num: usize, + ) -> SyncAggregate { + let fork_version_slot = signature_slot.max(1.into()) - 1; + let fork_version = + compute_fork_version(ctx, compute_epoch_at_slot(ctx, fork_version_slot)); + let domain = compute_domain( + ctx, + DOMAIN_SYNC_COMMITTEE, + Some(fork_version), + Some(ctx.genesis_validators_root()), + ) + .unwrap(); + let signing_root = compute_signing_root(attested_header, domain).unwrap(); + self.sign(signing_root, sign_num) + } + + pub fn sign( + &self, + signing_root: H256, + sign_num: usize, + ) -> SyncAggregate { + // let mut sigs = Vec::new(); + let mut agg_sig = AggregateSignature::new(); + let mut sg = SyncAggregate::::default(); + for (i, v) in self.committee.iter().enumerate() { + if i < sign_num { + agg_sig.add(&v.sign(signing_root)); + sg.sync_committee_bits.set(i, true); + } else { + sg.sync_committee_bits.set(i, false); + } + } + sg.sync_committee_signature = + Signature::try_from(agg_sig.as_bytes().to_vec()).unwrap(); + sg + } + } + + pub struct MockSyncCommitteeManager { + pub base_period: u64, + pub committees: Vec>, + } + + impl MockSyncCommitteeManager { + pub fn new(base_period: u64, n_period: u64) -> Self { + let mut committees = Vec::new(); + for _ in 0..n_period { + committees.push(MockSyncCommittee::::new()); + } + Self { + base_period, + committees, + } + } + + pub fn get_committee(&self, period: u64) -> &MockSyncCommittee { + let idx = period - self.base_period; + &self.committees[idx as usize] + } + } + + pub fn gen_light_client_update< + const SYNC_COMMITTEE_SIZE: usize, + C: ChainConsensusVerificationContext, + >( + ctx: &C, + signature_slot: Slot, + attested_slot: Slot, + finalized_epoch: Epoch, + execution_state_root: H256, + execution_block_number: BlockNumber, + scm: &MockSyncCommitteeManager, + ) -> ConsensusUpdateInfo { + let signature_period = compute_sync_committee_period_at_slot(ctx, signature_slot); + gen_light_client_update_with_params( + ctx, + signature_slot, + attested_slot, + finalized_epoch, + execution_state_root, + execution_block_number, + scm.get_committee(signature_period.into()), + scm.get_committee((signature_period + 1).into()), + SYNC_COMMITTEE_SIZE, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn gen_light_client_update_with_params< + const SYNC_COMMITTEE_SIZE: usize, + C: ChainConsensusVerificationContext, + >( + ctx: &C, + signature_slot: Slot, + attested_slot: Slot, + finalized_epoch: Epoch, + execution_state_root: H256, + execution_block_number: BlockNumber, + current_sync_committee: &MockSyncCommittee, + next_sync_committee: &MockSyncCommittee, + sign_num: usize, + ) -> ConsensusUpdateInfo { + assert!( + sign_num <= SYNC_COMMITTEE_SIZE, + "sign_num must be less than SYNC_COMMITTEE_SIZE({})", + SYNC_COMMITTEE_SIZE + ); + let finalized_block = gen_finalized_beacon_block::( + ctx, + finalized_epoch, + execution_state_root, + execution_block_number, + ); + let finalized_root = hash_tree_root(finalized_block.clone()).unwrap(); + let (attested_block, finalized_checkpoint_branch, _, next_sync_committee_branch) = + gen_attested_beacon_block( + ctx, + attested_slot, + finalized_root, + current_sync_committee.to_committee(), + next_sync_committee.to_committee(), + ); + + let (_, finalized_execution_branch) = + ethereum_consensus::fork::deneb::gen_execution_payload_proof(&finalized_block.body) + .unwrap(); + let finalized_execution_root = + hash_tree_root(finalized_block.body.execution_payload.clone()) + .unwrap() + .0 + .into(); + + let attested_header = attested_block.to_header(); + let update = LightClientUpdate:: { + attested_header: attested_header.clone(), + finalized_header: (finalized_block.to_header(), finalized_checkpoint_branch), + signature_slot, + sync_aggregate: current_sync_committee.sign_header( + ctx, + signature_slot, + attested_header, + sign_num, + ), + next_sync_committee: Some(( + next_sync_committee.to_committee(), + next_sync_committee_branch, + )), + }; + + ConsensusUpdateInfo { + light_client_update: update, + finalized_execution_root, + finalized_execution_branch, + } + } + + fn compute_epoch_boundary_slot(ctx: &C, epoch: Epoch) -> Slot { + ctx.slots_per_epoch() * epoch + } + + pub fn gen_attested_beacon_block( + _: &C, + attested_slot: Slot, + finalized_header_root: H256, + current_sync_committee: SyncCommittee, + next_sync_committee: SyncCommittee, + ) -> (DenebBeaconBlock, Vec, Vec, Vec) { + let mut block = DenebBeaconBlock { + slot: attested_slot, + ..Default::default() + }; + + let finalized_checkpoint = Checkpoint { + root: finalized_header_root, + ..Default::default() + }; + let state = DummyDenebBeaconState::::new( + attested_slot.into(), + finalized_checkpoint, + current_sync_committee, + next_sync_committee, + ); + block.state_root = state.tree().root().unwrap().into(); + + let finalized_checkpoint_proof = state.generate_finalized_checkpoint(); + let current_sync_committee_proof = state.generate_current_sync_committee_proof(); + let next_sync_committee_proof = state.generate_next_sync_committee_proof(); + ( + block, + finalized_checkpoint_proof, + current_sync_committee_proof, + next_sync_committee_proof, ) - .unwrap(), - min_genesis_time: U64(1578009600), + } + + pub fn gen_finalized_beacon_block( + ctx: &C, + finalized_epoch: Epoch, + execution_state_root: H256, + execution_block_number: BlockNumber, + ) -> DenebBeaconBlock { + let mut block = DenebBeaconBlock { + slot: compute_epoch_boundary_slot(ctx, finalized_epoch), + ..Default::default() + }; + let mut body = deneb::BeaconBlockBody::default(); + body.execution_payload.state_root = execution_state_root; + body.execution_payload.block_number = execution_block_number; + block.body = body; + block + } + + pub type DummySSZType = [u8; 32]; + + /// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate + #[derive(Debug, Clone, Default)] + struct DummyDenebBeaconState { + genesis_time: DummySSZType, + genesis_validators_root: DummySSZType, + pub slot: U64, + fork: DummySSZType, + latest_block_header: DummySSZType, + block_roots: DummySSZType, + state_roots: DummySSZType, + historical_roots: DummySSZType, + eth1_data: DummySSZType, + eth1_data_votes: DummySSZType, + eth1_deposit_index: DummySSZType, + validators: DummySSZType, + balances: DummySSZType, + randao_mixes: DummySSZType, + slashings: DummySSZType, + previous_epoch_participation: DummySSZType, + current_epoch_participation: DummySSZType, + justification_bits: DummySSZType, + previous_justified_checkpoint: DummySSZType, + current_justified_checkpoint: DummySSZType, + pub finalized_checkpoint: Checkpoint, + inactivity_scores: DummySSZType, + pub current_sync_committee: SyncCommittee, + pub next_sync_committee: SyncCommittee, + latest_execution_payload_header: DummySSZType, + next_withdrawal_index: DummySSZType, + next_withdrawal_validator_index: DummySSZType, + historical_summaries: DummySSZType, + } + + impl DummyDenebBeaconState { + pub fn new( + slot: u64, + finalized_checkpoint: Checkpoint, + current_sync_committee: SyncCommittee, + next_sync_committee: SyncCommittee, + ) -> Self { + Self { + slot: slot.into(), + finalized_checkpoint, + current_sync_committee, + next_sync_committee, + ..Default::default() + } + } + + pub fn tree(&self) -> MerkleTree { + use ethereum_consensus::compute::hash_tree_root; + let tree = MerkleTree::from_leaves( + ([ + self.genesis_time, + self.genesis_validators_root, + hash_tree_root(self.slot).unwrap().0, + self.fork, + self.latest_block_header, + self.block_roots, + self.state_roots, + self.historical_roots, + self.eth1_data, + self.eth1_data_votes, + self.eth1_deposit_index, + self.validators, + self.balances, + self.randao_mixes, + self.slashings, + self.previous_epoch_participation, + self.current_epoch_participation, + self.justification_bits, + self.previous_justified_checkpoint, + self.current_justified_checkpoint, + hash_tree_root(self.finalized_checkpoint.clone()).unwrap().0, + self.inactivity_scores, + hash_tree_root(self.current_sync_committee.clone()) + .unwrap() + .0, + hash_tree_root(self.next_sync_committee.clone()).unwrap().0, + self.latest_execution_payload_header, + self.next_withdrawal_index, + self.next_withdrawal_validator_index, + self.historical_summaries, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ] as [_; 32]) + .as_ref(), + ); + tree + } + + pub fn generate_finalized_checkpoint(&self) -> Vec { + let br: Vec = self + .tree() + .proof(&[20]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect(); + let node = hash_tree_root(self.finalized_checkpoint.epoch) + .unwrap() + .0 + .into(); + let mut branch: Vec = Vec::new(); + branch.push(node); + for b in br.iter() { + branch.push(*b); + } + branch + } + + pub fn generate_current_sync_committee_proof(&self) -> Vec { + self.tree() + .proof(&[22]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect() + } + + pub fn generate_next_sync_committee_proof(&self) -> Vec { + self.tree() + .proof(&[23]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect() + } } } } diff --git a/crates/light-client-verifier/src/misbehaviour.rs b/crates/light-client-verifier/src/misbehaviour.rs index d35061d..92fc775 100644 --- a/crates/light-client-verifier/src/misbehaviour.rs +++ b/crates/light-client-verifier/src/misbehaviour.rs @@ -80,20 +80,21 @@ impl> NextSyncCommitteeMisbehaviour { pub fn validate_basic(&self, ctx: &CC) -> Result<(), Error> { - let period_1 = compute_sync_committee_period_at_slot( + let attested_period_1 = compute_sync_committee_period_at_slot( ctx, self.consensus_update_1.attested_beacon_header().slot, ); - let period_2 = compute_sync_committee_period_at_slot( + let attested_period_2 = compute_sync_committee_period_at_slot( ctx, self.consensus_update_2.attested_beacon_header().slot, ); let next_1 = self.consensus_update_1.next_sync_committee(); let next_2 = self.consensus_update_2.next_sync_committee(); - if period_1 != period_2 { + if attested_period_1 != attested_period_2 { Err(Error::DifferentPeriodInNextSyncCommitteeMisbehaviour( - period_1, period_2, + attested_period_1, + attested_period_2, )) } else if next_1.is_none() || next_2.is_none() { Err(Error::NoNextSyncCommitteeInNextSyncCommitteeMisbehaviour) diff --git a/crates/light-client-verifier/src/updates.rs b/crates/light-client-verifier/src/updates.rs index d9d91c0..f1555bd 100644 --- a/crates/light-client-verifier/src/updates.rs +++ b/crates/light-client-verifier/src/updates.rs @@ -119,3 +119,14 @@ pub trait ExecutionUpdate: core::fmt::Debug + Clone + PartialEq + Eq { Ok(()) } } + +pub type LightClientBootstrapInfo = + bellatrix::LightClientBootstrapInfo; + +pub type LightClientUpdate = + ethereum_consensus::fork::bellatrix::LightClientUpdate; + +pub type ConsensusUpdateInfo = + bellatrix::ConsensusUpdateInfo; + +pub type ExecutionUpdateInfo = bellatrix::ExecutionUpdateInfo; From a0404c7433ea33ceb1c4dd9d9d01134bf882564a Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Mon, 4 Nov 2024 14:12:55 +0900 Subject: [PATCH 2/5] refactor relevant update checks Signed-off-by: Jun Kimura --- crates/consensus/src/fork/deneb.rs | 139 ++++++++-------- crates/light-client-cli/src/client.rs | 37 +---- crates/light-client-cli/src/errors.rs | 5 + crates/light-client-cli/src/state.rs | 148 +++++++++++++++++- crates/light-client-verifier/src/consensus.rs | 116 ++++---------- crates/light-client-verifier/src/errors.rs | 12 +- crates/light-client-verifier/src/mock.rs | 77 ++++++--- crates/light-client-verifier/src/state.rs | 80 ++-------- crates/light-client-verifier/src/updates.rs | 38 +++-- 9 files changed, 348 insertions(+), 304 deletions(-) diff --git a/crates/consensus/src/fork/deneb.rs b/crates/consensus/src/fork/deneb.rs index afbce0b..985dd6d 100644 --- a/crates/consensus/src/fork/deneb.rs +++ b/crates/consensus/src/fork/deneb.rs @@ -365,72 +365,76 @@ pub fn gen_execution_payload_field_proof< )) } -pub fn gen_execution_payload_proof< - const MAX_PROPOSER_SLASHINGS: usize, - const MAX_VALIDATORS_PER_COMMITTEE: usize, - const MAX_ATTESTER_SLASHINGS: usize, - const MAX_ATTESTATIONS: usize, - const DEPOSIT_CONTRACT_TREE_DEPTH: usize, - const MAX_DEPOSITS: usize, - const MAX_VOLUNTARY_EXITS: usize, - const BYTES_PER_LOGS_BLOOM: usize, - const MAX_EXTRA_DATA_BYTES: usize, - const MAX_BYTES_PER_TRANSACTION: usize, - const MAX_TRANSACTIONS_PER_PAYLOAD: usize, - const MAX_WITHDRAWALS_PER_PAYLOAD: usize, - const MAX_BLS_TO_EXECUTION_CHANGES: usize, - const SYNC_COMMITTEE_SIZE: usize, - const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize, ->( - body: &BeaconBlockBody< - MAX_PROPOSER_SLASHINGS, - MAX_VALIDATORS_PER_COMMITTEE, - MAX_ATTESTER_SLASHINGS, - MAX_ATTESTATIONS, - DEPOSIT_CONTRACT_TREE_DEPTH, - MAX_DEPOSITS, - MAX_VOLUNTARY_EXITS, - BYTES_PER_LOGS_BLOOM, - MAX_EXTRA_DATA_BYTES, - MAX_BYTES_PER_TRANSACTION, - MAX_TRANSACTIONS_PER_PAYLOAD, - MAX_WITHDRAWALS_PER_PAYLOAD, - MAX_BLS_TO_EXECUTION_CHANGES, - SYNC_COMMITTEE_SIZE, - MAX_BLOB_COMMITMENTS_PER_BLOCK, - >, -) -> Result<(Root, Vec), Error> { - let tree = MerkleTree::from_leaves( - ([ - hash_tree_root(body.randao_reveal.clone()).unwrap().0, - hash_tree_root(body.eth1_data.clone()).unwrap().0, - body.graffiti.0, - hash_tree_root(body.proposer_slashings.clone()).unwrap().0, - hash_tree_root(body.attester_slashings.clone()).unwrap().0, - hash_tree_root(body.attestations.clone()).unwrap().0, - hash_tree_root(body.deposits.clone()).unwrap().0, - hash_tree_root(body.voluntary_exits.clone()).unwrap().0, - hash_tree_root(body.sync_aggregate.clone()).unwrap().0, - hash_tree_root(body.execution_payload.clone()).unwrap().0, - hash_tree_root(body.bls_to_execution_changes.clone()) - .unwrap() - .0, - hash_tree_root(body.blob_kzg_commitments.clone()).unwrap().0, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ] as [_; 16]) - .as_ref(), - ); - Ok(( - H256(tree.root().unwrap()), - tree.proof(&[get_subtree_index(DENEB_FORK_SPEC.execution_payload_gindex) as usize]) - .proof_hashes() - .iter() - .map(|h| H256::from_slice(h)) - .collect(), - )) +pub mod test_utils { + use super::*; + + pub fn gen_execution_payload_proof< + const MAX_PROPOSER_SLASHINGS: usize, + const MAX_VALIDATORS_PER_COMMITTEE: usize, + const MAX_ATTESTER_SLASHINGS: usize, + const MAX_ATTESTATIONS: usize, + const DEPOSIT_CONTRACT_TREE_DEPTH: usize, + const MAX_DEPOSITS: usize, + const MAX_VOLUNTARY_EXITS: usize, + const BYTES_PER_LOGS_BLOOM: usize, + const MAX_EXTRA_DATA_BYTES: usize, + const MAX_BYTES_PER_TRANSACTION: usize, + const MAX_TRANSACTIONS_PER_PAYLOAD: usize, + const MAX_WITHDRAWALS_PER_PAYLOAD: usize, + const MAX_BLS_TO_EXECUTION_CHANGES: usize, + const SYNC_COMMITTEE_SIZE: usize, + const MAX_BLOB_COMMITMENTS_PER_BLOCK: usize, + >( + body: &BeaconBlockBody< + MAX_PROPOSER_SLASHINGS, + MAX_VALIDATORS_PER_COMMITTEE, + MAX_ATTESTER_SLASHINGS, + MAX_ATTESTATIONS, + DEPOSIT_CONTRACT_TREE_DEPTH, + MAX_DEPOSITS, + MAX_VOLUNTARY_EXITS, + BYTES_PER_LOGS_BLOOM, + MAX_EXTRA_DATA_BYTES, + MAX_BYTES_PER_TRANSACTION, + MAX_TRANSACTIONS_PER_PAYLOAD, + MAX_WITHDRAWALS_PER_PAYLOAD, + MAX_BLS_TO_EXECUTION_CHANGES, + SYNC_COMMITTEE_SIZE, + MAX_BLOB_COMMITMENTS_PER_BLOCK, + >, + ) -> Result<(Root, Vec), Error> { + let tree = MerkleTree::from_leaves( + ([ + hash_tree_root(body.randao_reveal.clone()).unwrap().0, + hash_tree_root(body.eth1_data.clone()).unwrap().0, + body.graffiti.0, + hash_tree_root(body.proposer_slashings.clone()).unwrap().0, + hash_tree_root(body.attester_slashings.clone()).unwrap().0, + hash_tree_root(body.attestations.clone()).unwrap().0, + hash_tree_root(body.deposits.clone()).unwrap().0, + hash_tree_root(body.voluntary_exits.clone()).unwrap().0, + hash_tree_root(body.sync_aggregate.clone()).unwrap().0, + hash_tree_root(body.execution_payload.clone()).unwrap().0, + hash_tree_root(body.bls_to_execution_changes.clone()) + .unwrap() + .0, + hash_tree_root(body.blob_kzg_commitments.clone()).unwrap().0, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ] as [_; 16]) + .as_ref(), + ); + Ok(( + H256(tree.root().unwrap()), + tree.proof(&[get_subtree_index(DENEB_FORK_SPEC.execution_payload_gindex) as usize]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect(), + )) + } } #[cfg(test)] @@ -459,7 +463,8 @@ mod tests { block.hash_tree_root().unwrap() ); - let (block_root, payload_proof) = gen_execution_payload_proof(&block.body).unwrap(); + let (block_root, payload_proof) = + test_utils::gen_execution_payload_proof(&block.body).unwrap(); assert_eq!( block_root.as_bytes(), block.body.hash_tree_root().unwrap().as_bytes() diff --git a/crates/light-client-cli/src/client.rs b/crates/light-client-cli/src/client.rs index b1d7134..ffb0358 100644 --- a/crates/light-client-cli/src/client.rs +++ b/crates/light-client-cli/src/client.rs @@ -16,8 +16,7 @@ use ethereum_consensus::{ use ethereum_light_client_verifier::{ consensus::SyncProtocolVerifier, context::{ChainConsensusVerificationContext, Fraction, LightClientContext}, - state::should_update_sync_committees, - updates::{deneb::ConsensusUpdateInfo, ConsensusUpdate}, + updates::deneb::ConsensusUpdateInfo, }; use log::*; use std::time::SystemTime; @@ -268,37 +267,9 @@ impl< self.verifier .validate_updates(vctx, state, &updates.0, &updates.1)?; - self.apply_light_client_update(state, updates.0) - } - - fn apply_light_client_update( - &self, - state: &LightClientStore, - consensus_update: ConsensusUpdateInfo< - SYNC_COMMITTEE_SIZE, - BYTES_PER_LOGS_BLOOM, - MAX_EXTRA_DATA_BYTES, - >, - ) -> Result< - Option>, - > { - let mut new_state = state.clone(); - let (current_committee, next_committee) = - should_update_sync_committees(&self.ctx, state, &consensus_update)?; - if let Some(current_committee) = current_committee { - new_state.current_sync_committee = current_committee.clone(); - } - if let Some(next_committee) = next_committee { - new_state.next_sync_committee = next_committee.cloned(); - } - if consensus_update.finalized_beacon_header().slot > state.latest_finalized_header.slot { - new_state.latest_finalized_header = consensus_update.finalized_beacon_header().clone(); - new_state.latest_execution_payload_header = - consensus_update.finalized_header.execution.clone(); - } - if *state != new_state { - self.ctx.store_light_client_state(&new_state)?; - Ok(Some(new_state)) + if let Some(new_store) = state.apply_light_client_update(vctx, &updates.0)? { + self.ctx.store_light_client_state(&new_store)?; + Ok(Some(new_store)) } else { Ok(None) } diff --git a/crates/light-client-cli/src/errors.rs b/crates/light-client-cli/src/errors.rs index d39b8d1..9b142a2 100644 --- a/crates/light-client-cli/src/errors.rs +++ b/crates/light-client-cli/src/errors.rs @@ -1,4 +1,5 @@ use displaydoc::Display; +use ethereum_consensus::sync_protocol::SyncCommitteePeriod; #[derive(Debug, Display)] pub enum Error { @@ -14,6 +15,10 @@ pub enum Error { CommontError(ethereum_consensus::errors::Error), /// finalized header not found FinalizedHeaderNotFound, + /// unexpected attested period: `store={0} attested={1} reason={2}` + UnexpectedAttestedPeriod(SyncCommitteePeriod, SyncCommitteePeriod, String), + /// cannot rotate to next sync committee: `store={0} finalized={1}` + CannotRotateNextSyncCommittee(SyncCommitteePeriod, SyncCommitteePeriod), /// other error: `{description}` Other { description: String }, } diff --git a/crates/light-client-cli/src/state.rs b/crates/light-client-cli/src/state.rs index 668ea6a..7c8edad 100644 --- a/crates/light-client-cli/src/state.rs +++ b/crates/light-client-cli/src/state.rs @@ -1,10 +1,15 @@ use ethereum_consensus::{ beacon::{BeaconBlockHeader, Slot}, + compute::compute_sync_committee_period_at_slot, fork::deneb::{ExecutionPayloadHeader, LightClientBootstrap}, sync_protocol::SyncCommittee, types::{H256, U64}, }; -use ethereum_light_client_verifier::{state::LightClientStoreReader, updates::ExecutionUpdate}; +use ethereum_light_client_verifier::{ + context::ChainConsensusVerificationContext, + state::LightClientStoreReader, + updates::{ConsensusUpdate, ExecutionUpdate}, +}; #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct LightClientStore< @@ -43,6 +48,69 @@ impl< next_sync_committee: None, } } + + pub fn current_slot(&self) -> Slot { + self.latest_finalized_header.slot + } + + pub fn current_period( + &self, + ctx: &CC, + ) -> ethereum_consensus::sync_protocol::SyncCommitteePeriod { + compute_sync_committee_period_at_slot(ctx, self.current_slot()) + } + + pub fn apply_light_client_update< + CC: ChainConsensusVerificationContext, + CU: ConsensusUpdate, + >( + &self, + ctx: &CC, + consensus_update: &CU, + ) -> Result, crate::errors::Error> { + let mut new_store = self.clone(); + let store_period = + compute_sync_committee_period_at_slot(ctx, new_store.latest_finalized_header.slot); + let attested_period = compute_sync_committee_period_at_slot( + ctx, + consensus_update.attested_beacon_header().slot, + ); + + let mut updated = if store_period == attested_period { + if let Some(committee) = consensus_update.next_sync_committee() { + new_store.next_sync_committee = Some(committee.clone()); + true + } else { + false + } + } else if store_period + 1 == attested_period { + if let Some(committee) = new_store.next_sync_committee.as_ref() { + new_store.current_sync_committee = committee.clone(); + new_store.next_sync_committee = consensus_update.next_sync_committee().cloned(); + true + } else { + return Err(crate::errors::Error::CannotRotateNextSyncCommittee( + store_period, + attested_period, + )); + } + } else { + return Err(crate::errors::Error::UnexpectedAttestedPeriod( + store_period, + attested_period, + "attested period must be equal to store_period or store_period+1".into(), + )); + }; + if consensus_update.finalized_beacon_header().slot > self.latest_finalized_header.slot { + new_store.latest_finalized_header = consensus_update.finalized_beacon_header().clone(); + updated = true; + } + if updated { + Ok(Some(new_store)) + } else { + Ok(None) + } + } } impl< @@ -52,16 +120,80 @@ impl< > LightClientStoreReader for LightClientStore { - fn current_slot(&self) -> Slot { - self.latest_finalized_header.slot + fn get_sync_committee( + &self, + ctx: &CC, + period: ethereum_consensus::sync_protocol::SyncCommitteePeriod, + ) -> Option> { + // https://github.com/ethereum/consensus-specs/blob/1b408e9354358cd7f883c170813e8bf93c922a94/specs/altair/light-client/sync-protocol.md#validate_light_client_update + // # Verify sync committee aggregate signature + // if update_signature_period == store_period: + // sync_committee = store.current_sync_committee + // else: + // sync_committee = store.next_sync_committee + let current_period = self.current_period(ctx); + if period == current_period { + Some(self.current_sync_committee.clone()) + } else if period == current_period + 1 { + self.next_sync_committee.clone() + } else { + None + } } - fn current_sync_committee(&self) -> &SyncCommittee { - &self.current_sync_committee - } + fn ensure_relevant_update< + CC: ethereum_consensus::context::ChainContext, + C: ethereum_light_client_verifier::updates::ConsensusUpdate, + >( + &self, + ctx: &CC, + update: &C, + ) -> Result<(), ethereum_light_client_verifier::errors::Error> { + update.ensure_consistent_update_period(ctx)?; + + let store_period = compute_sync_committee_period_at_slot(ctx, self.current_slot()); + let update_attested_period = + compute_sync_committee_period_at_slot(ctx, update.attested_beacon_header().slot); + let update_has_next_sync_committee = self.next_sync_committee.is_none() + && (update.next_sync_committee().is_some() && update_attested_period == store_period); - fn next_sync_committee(&self) -> Option<&SyncCommittee> { - self.next_sync_committee.as_ref() + // https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#validate_light_client_update + // assert (update_attested_slot > store.finalized_header.beacon.slot or update_has_next_sync_committee) + if !(update.attested_beacon_header().slot > self.current_slot() + || update_has_next_sync_committee) + { + return Err(ethereum_light_client_verifier::errors::Error::IrrelevantConsensusUpdates(format!( + "attested_beacon_header_slot={} store_slot={} update_has_next_sync_committee={} is_next_sync_committee_known={}", + update.attested_beacon_header().slot, + self.current_slot(), + update_has_next_sync_committee, + self.next_sync_committee.is_some() + ))); + } + + // https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#process_light_client_update + // update_has_finalized_next_sync_committee = ( + // not is_next_sync_committee_known(store) + // and is_sync_committee_update(update) and is_finality_update(update) and ( + // compute_sync_committee_period_at_slot(update.finalized_header.beacon.slot) + // == compute_sync_committee_period_at_slot(update.attested_header.beacon.slot) + // ) + // ) + let update_has_finalized_next_sync_committee = + self.next_sync_committee.is_none() && update.next_sync_committee().is_some(); // equivalent to is_sync_committee_update(update) + + // https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#process_light_client_update + // update.finalized_header.beacon.slot > store.finalized_header.beacon.slot + // or update_has_finalized_next_sync_committee + if !(update_has_finalized_next_sync_committee + || update.finalized_beacon_header().slot > self.current_slot()) + { + return Err(ethereum_light_client_verifier::errors::Error::IrrelevantConsensusUpdates(format!( + "finalized_beacon_header_slot={} store_slot={} update_has_finalized_next_sync_committee={}", + update.finalized_beacon_header().slot, self.current_slot(), update_has_finalized_next_sync_committee + ))); + } + Ok(()) } } diff --git a/crates/light-client-verifier/src/consensus.rs b/crates/light-client-verifier/src/consensus.rs index aca8af2..035d51f 100644 --- a/crates/light-client-verifier/src/consensus.rs +++ b/crates/light-client-verifier/src/consensus.rs @@ -87,7 +87,7 @@ impl Result<(), Error> { consensus_update.validate_basic(ctx)?; - self.ensure_relevant_update(ctx, store, consensus_update)?; + store.ensure_relevant_update(ctx, consensus_update)?; let sync_committee = self.get_sync_committee(ctx, store, consensus_update)?; validate_light_client_update(ctx, store, consensus_update)?; verify_sync_committee_attestation(ctx, consensus_update, &sync_committee)?; @@ -148,62 +148,6 @@ impl>( - &self, - ctx: &CC, - store: &ST, - update: &CU, - ) -> Result<(), Error> { - let store_period = compute_sync_committee_period_at_slot(ctx, store.current_slot()); - - let update_attested_period = - compute_sync_committee_period_at_slot(ctx, update.attested_beacon_header().slot); - let update_has_next_sync_committee = store.next_sync_committee().is_none() - && (update.next_sync_committee().is_some() && update_attested_period == store_period); - - // https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#validate_light_client_update - // assert (update_attested_slot > store.finalized_header.beacon.slot or update_has_next_sync_committee) - if !(update.attested_beacon_header().slot > store.current_slot() - || update_has_next_sync_committee) - { - return Err(Error::IrrelevantConsensusUpdates(format!( - "attested_beacon_header_slot={} store_slot={} update_has_next_sync_committee={} is_next_sync_committee_known={}", - update.attested_beacon_header().slot, - store.current_slot(), - update_has_next_sync_committee, - store.next_sync_committee().is_some() - ))); - } - - // https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#process_light_client_update - // update_has_finalized_next_sync_committee = ( - // not is_next_sync_committee_known(store) - // and is_sync_committee_update(update) and is_finality_update(update) and ( - // compute_sync_committee_period_at_slot(update.finalized_header.beacon.slot) - // == compute_sync_committee_period_at_slot(update.attested_header.beacon.slot) - // ) - // ) - let update_has_finalized_next_sync_committee = store.next_sync_committee().is_none() - && update.next_sync_committee().is_some() // equivalent to is_sync_committee_update(update) - && compute_sync_committee_period_at_slot(ctx, update.finalized_beacon_header().slot) - == update_attested_period; - - // https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#process_light_client_update - // update.finalized_header.beacon.slot > store.finalized_header.beacon.slot - // or update_has_finalized_next_sync_committee - if !(update_has_finalized_next_sync_committee - || update.finalized_beacon_header().slot > store.current_slot()) - { - return Err(Error::IrrelevantConsensusUpdates(format!( - "finalized_beacon_header_slot={} store_slot={} update_has_finalized_next_sync_committee={}", - update.finalized_beacon_header().slot, store.current_slot(), update_has_finalized_next_sync_committee - ))); - } - - Ok(()) - } - /// get the sync committee from the store pub fn get_sync_committee>( &self, @@ -211,32 +155,15 @@ impl Result, Error> { - let store_period = compute_sync_committee_period_at_slot(ctx, store.current_slot()); let update_signature_period = compute_sync_committee_period_at_slot(ctx, update.signature_slot()); - - // https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#validate_light_client_update - // # Verify sync committee aggregate signature - // if update_signature_period == store_period: - // sync_committee = store.current_sync_committee - // else: - // sync_committee = store.next_sync_committee - if update_signature_period == store_period { - Ok(store.current_sync_committee().clone()) - } else if update_signature_period == store_period + 1 { - if let Some(next_sync_committee) = store.next_sync_committee() { - Ok(next_sync_committee.clone()) - } else { - Err(Error::NoNextSyncCommitteeInStore( - store_period.into(), - update_signature_period.into(), - )) - } + if let Some(committee) = store.get_sync_committee(ctx, update_signature_period) { + Ok(committee) } else { Err(Error::UnexpectedSingaturePeriod( - store_period, update_signature_period, - "signature period must be equal to store_period or store_period+1".into(), + "store does not have the sync committee corresponding to the update signature period" + .into(), )) } } @@ -252,6 +179,24 @@ pub fn verify_sync_committee_attestation< consensus_update: &CU, sync_committee: &SyncCommittee, ) -> Result<(), Error> { + // ensure that suffienct participants exist + let participants = consensus_update.sync_aggregate().count_participants(); + // from the spec: `assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS` + if participants < ctx.min_sync_committee_participants() { + return Err(Error::LessThanMinimalParticipants( + participants, + ctx.min_sync_committee_participants(), + )); + } else if participants as u64 * ctx.signature_threshold().denominator + < consensus_update.sync_aggregate().sync_committee_bits.len() as u64 + * ctx.signature_threshold().numerator + { + return Err(Error::InsufficientParticipants( + participants as u64, + consensus_update.sync_aggregate().sync_committee_bits.len() as u64, + )); + } + let participant_pubkeys: Vec = consensus_update .sync_aggregate() .sync_committee_bits @@ -364,13 +309,10 @@ pub fn validate_light_client_update< ctx, consensus_update.attested_beacon_header().slot, ); - let store_period = compute_sync_committee_period_at_slot(ctx, store.current_slot()); - if let Some(store_next_sync_committee) = store.next_sync_committee() { - if update_attested_period == store_period - && store_next_sync_committee != update_next_sync_committee - { + if let Some(committee) = store.get_sync_committee(ctx, update_attested_period + 1) { + if committee != *update_next_sync_committee { return Err(Error::InconsistentNextSyncCommittee( - store_next_sync_committee.aggregate_pubkey.clone(), + committee.aggregate_pubkey.clone(), update_next_sync_committee.aggregate_pubkey.clone(), )); } @@ -950,8 +892,10 @@ mod tests { ); let (_, finalized_execution_branch) = - ethereum_consensus::fork::deneb::gen_execution_payload_proof(&finalized_block.body) - .unwrap(); + ethereum_consensus::fork::deneb::test_utils::gen_execution_payload_proof( + &finalized_block.body, + ) + .unwrap(); let finalized_execution_root = hash_tree_root(finalized_block.body.execution_payload.clone()) .unwrap() diff --git a/crates/light-client-verifier/src/errors.rs b/crates/light-client-verifier/src/errors.rs index 1d678dd..7c94c17 100644 --- a/crates/light-client-verifier/src/errors.rs +++ b/crates/light-client-verifier/src/errors.rs @@ -13,12 +13,12 @@ type BoxedTrieError = Box>; #[derive(Debug, Display)] pub enum Error { - /// unexpected signature period: `store={0} signature={1} reason={2}` - UnexpectedSingaturePeriod(SyncCommitteePeriod, SyncCommitteePeriod, String), - /// invalid finalized period: `store={0} finalized={1} reason={2}` - InvalidFinalizedPeriod(SyncCommitteePeriod, SyncCommitteePeriod, String), - /// not finalized period: `finalized={0} attested={1}` - NotFinalizedUpdate(SyncCommitteePeriod, SyncCommitteePeriod), + /// unexpected signature period: `signature={0} reason={1}` + UnexpectedSingaturePeriod(SyncCommitteePeriod, String), + /// unexpected attested period: `store={0} attested={1} reason={2}` + UnexpectedAttestedPeriod(SyncCommitteePeriod, SyncCommitteePeriod, String), + /// inconsistent update period: `store={0} attested={1}` + InconsistentUpdatePeriod(SyncCommitteePeriod, SyncCommitteePeriod), /// cannot rotate to next sync committee: `store={0} finalized={1}` CannotRotateNextSyncCommittee(SyncCommitteePeriod, SyncCommitteePeriod), /// no next sync committee in store: `store_period={0} signature_period={1}` diff --git a/crates/light-client-verifier/src/mock.rs b/crates/light-client-verifier/src/mock.rs index 771913c..aeb6660 100644 --- a/crates/light-client-verifier/src/mock.rs +++ b/crates/light-client-verifier/src/mock.rs @@ -1,6 +1,7 @@ use crate::context::ConsensusVerificationContext; -use crate::state::{should_update_sync_committees, LightClientStoreReader}; +use crate::state::LightClientStoreReader; use crate::updates::ConsensusUpdate; +use ethereum_consensus::compute::compute_sync_committee_period_at_slot; use ethereum_consensus::context::ChainContext; use ethereum_consensus::sync_protocol::SyncCommittee; use ethereum_consensus::{ @@ -30,6 +31,10 @@ impl MockStore { } } + pub fn current_period(&self, ctx: &CC) -> Slot { + compute_sync_committee_period_at_slot(ctx, self.latest_finalized_header.slot) + } + pub fn apply_light_client_update< CC: ChainContext + ConsensusVerificationContext, CU: ConsensusUpdate, @@ -38,17 +43,38 @@ impl MockStore { ctx: &CC, consensus_update: &CU, ) -> Result { - let (current_committee, next_committee) = - should_update_sync_committees(ctx, self, consensus_update)?; - let mut updated = false; - if let Some(committee) = current_committee { - self.current_sync_committee = committee.clone(); - updated = true; - } - if let Some(committee) = next_committee { - self.next_sync_committee = committee.cloned(); - updated = true; - } + let store_period = + compute_sync_committee_period_at_slot(ctx, self.latest_finalized_header.slot); + let attested_period = compute_sync_committee_period_at_slot( + ctx, + consensus_update.attested_beacon_header().slot, + ); + + let mut updated = if store_period == attested_period { + if let Some(committee) = consensus_update.next_sync_committee() { + self.next_sync_committee = Some(committee.clone()); + true + } else { + false + } + } else if store_period + 1 == attested_period { + if let Some(committee) = self.next_sync_committee.as_ref() { + self.current_sync_committee = committee.clone(); + self.next_sync_committee = consensus_update.next_sync_committee().cloned(); + true + } else { + return Err(crate::errors::Error::CannotRotateNextSyncCommittee( + store_period, + attested_period, + )); + } + } else { + return Err(crate::errors::Error::UnexpectedAttestedPeriod( + store_period, + attested_period, + "attested period must be equal to store_period or store_period+1".into(), + )); + }; if consensus_update.finalized_beacon_header().slot > self.latest_finalized_header.slot { self.latest_finalized_header = consensus_update.finalized_beacon_header().clone(); updated = true; @@ -60,15 +86,26 @@ impl MockStore { impl LightClientStoreReader for MockStore { - fn current_slot(&self) -> Slot { - self.latest_finalized_header.slot - } - - fn current_sync_committee(&self) -> &SyncCommittee { - &self.current_sync_committee + fn get_sync_committee( + &self, + ctx: &CC, + period: ethereum_consensus::sync_protocol::SyncCommitteePeriod, + ) -> Option> { + let current_period = self.current_period(ctx); + if period == current_period { + Some(self.current_sync_committee.clone()) + } else if period == current_period + 1 { + self.next_sync_committee.clone() + } else { + None + } } - fn next_sync_committee(&self) -> Option<&SyncCommittee> { - self.next_sync_committee.as_ref() + fn ensure_relevant_update>( + &self, + ctx: &CC, + update: &CU, + ) -> Result<(), crate::errors::Error> { + update.ensure_consistent_update_period(ctx) } } diff --git a/crates/light-client-verifier/src/state.rs b/crates/light-client-verifier/src/state.rs index d95cf03..b4722ac 100644 --- a/crates/light-client-verifier/src/state.rs +++ b/crates/light-client-verifier/src/state.rs @@ -1,71 +1,23 @@ use crate::{errors::Error, updates::ConsensusUpdate}; use ethereum_consensus::{ - beacon::Slot, compute::compute_sync_committee_period_at_slot, context::ChainContext, - sync_protocol::SyncCommittee, + context::ChainContext, + sync_protocol::{SyncCommittee, SyncCommitteePeriod}, }; pub trait LightClientStoreReader { - /// Returns the finalized slot based on the light client update. - fn current_slot(&self) -> Slot; - /// Returns the current sync committee based on the light client update. - fn current_sync_committee(&self) -> &SyncCommittee; - /// Returns the next sync committee based on the light client update. - fn next_sync_committee(&self) -> Option<&SyncCommittee>; -} - -/// Returns the new current and next sync committees based on the state and the consensus update. -/// -/// If the current sync committee should be updated, the new current sync committee is returned. -/// If the next sync committee should be updated, the new next sync committee is returned. -/// ref. https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#apply_light_client_update -pub fn should_update_sync_committees< - 's, - 'u, - const SYNC_COMMITTEE_SIZE: usize, - CC: ChainContext, - S: LightClientStoreReader, - CU: ConsensusUpdate, ->( - ctx: &CC, - state: &'s S, - consensus_update: &'u CU, -) -> Result< - ( - // new current sync committee - Option<&'s SyncCommittee>, - // new next sync committee - Option>>, - ), - Error, -> { - let store_period = compute_sync_committee_period_at_slot(ctx, state.current_slot()); - let update_finalized_period = - compute_sync_committee_period_at_slot(ctx, consensus_update.finalized_beacon_header().slot); - - if store_period != update_finalized_period && store_period + 1 != update_finalized_period { - return Err(Error::InvalidFinalizedPeriod( - store_period, - update_finalized_period, - "finalized period must be equal to store_period or store_period+1".into(), - )); - } + /// Returns the sync committee for the given period. + fn get_sync_committee( + &self, + ctx: &CC, + period: SyncCommitteePeriod, + ) -> Option>; - if let Some(store_next_sync_committee) = state.next_sync_committee() { - if update_finalized_period == store_period + 1 { - Ok(( - Some(store_next_sync_committee), - Some(consensus_update.next_sync_committee()), - )) - } else { - // no updates - Ok((None, None)) - } - } else if update_finalized_period == store_period { - Ok((None, Some(consensus_update.next_sync_committee()))) - } else { - Err(Error::CannotRotateNextSyncCommittee( - store_period, - update_finalized_period, - )) - } + /// Returns a error indicating whether the update is relevant to the light client store. + /// + /// This method should be used to determine whether the update should be applied to the store. + fn ensure_relevant_update>( + &self, + ctx: &CC, + update: &C, + ) -> Result<(), Error>; } diff --git a/crates/light-client-verifier/src/updates.rs b/crates/light-client-verifier/src/updates.rs index f1555bd..4f21d41 100644 --- a/crates/light-client-verifier/src/updates.rs +++ b/crates/light-client-verifier/src/updates.rs @@ -1,6 +1,8 @@ use crate::context::{ChainConsensusVerificationContext, ConsensusVerificationContext}; use crate::errors::Error; use crate::internal_prelude::*; +use ethereum_consensus::compute::compute_sync_committee_period_at_slot; +use ethereum_consensus::context::ChainContext; use ethereum_consensus::{ beacon::{BeaconBlockHeader, Slot}, merkle::is_valid_normalized_merkle_branch, @@ -41,6 +43,21 @@ pub trait ConsensusUpdate: fn sync_aggregate(&self) -> &SyncAggregate; fn signature_slot(&self) -> Slot; + fn ensure_consistent_update_period(&self, ctx: &C) -> Result<(), Error> { + let finalized_period = + compute_sync_committee_period_at_slot(ctx, self.finalized_beacon_header().slot); + let attested_period = + compute_sync_committee_period_at_slot(ctx, self.attested_beacon_header().slot); + if finalized_period == attested_period { + Ok(()) + } else { + Err(Error::InconsistentUpdatePeriod( + finalized_period, + attested_period, + )) + } + } + /// ref. https://github.com/ethereum/consensus-specs/blob/087e7378b44f327cdad4549304fc308613b780c3/specs/altair/light-client/sync-protocol.md#is_valid_light_client_header /// NOTE: There are no validation for the execution payload, so you should implement it if the update contains the execution payload. fn is_valid_light_client_finalized_header( @@ -57,6 +74,7 @@ pub trait ConsensusUpdate: .map_err(Error::InvalidFinalizedExecutionPayload) } + /// validate the basic properties of the update fn validate_basic(&self, ctx: &C) -> Result<(), Error> { // ensure that sync committee's aggreated key matches pubkeys if let Some(next_sync_committee) = self.next_sync_committee() { @@ -78,26 +96,6 @@ pub trait ConsensusUpdate: self.finalized_beacon_header().slot, )); } - - // ensure that suffienct participants exist - - let participants = self.sync_aggregate().count_participants(); - // from the spec: `assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS` - if participants < ctx.min_sync_committee_participants() { - return Err(Error::LessThanMinimalParticipants( - participants, - ctx.min_sync_committee_participants(), - )); - } else if participants as u64 * ctx.signature_threshold().denominator - < self.sync_aggregate().sync_committee_bits.len() as u64 - * ctx.signature_threshold().numerator - { - return Err(Error::InsufficientParticipants( - participants as u64, - self.sync_aggregate().sync_committee_bits.len() as u64, - )); - } - Ok(()) } } From 55a7a2a3e1200385b699430ac26e8761f5f318aa Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Mon, 4 Nov 2024 19:29:43 +0900 Subject: [PATCH 3/5] add tests Signed-off-by: Jun Kimura --- crates/light-client-verifier/src/consensus.rs | 367 ++++++++++++++---- .../light-client-verifier/src/misbehaviour.rs | 2 +- crates/light-client-verifier/src/mock.rs | 34 +- crates/light-client-verifier/src/state.rs | 4 +- 4 files changed, 301 insertions(+), 106 deletions(-) diff --git a/crates/light-client-verifier/src/consensus.rs b/crates/light-client-verifier/src/consensus.rs index 035d51f..8fc58dc 100644 --- a/crates/light-client-verifier/src/consensus.rs +++ b/crates/light-client-verifier/src/consensus.rs @@ -438,12 +438,6 @@ mod tests { ); assert!(verifier.validate_boostrap(&ctx, &bootstrap, None).is_ok()); - let mut store = MockStore::new( - bootstrap.beacon_header().clone(), - bootstrap.current_sync_committee().clone(), - execution_payload_state_root, - ); - let updates = [ "light_client_update_period_5.json", "light_client_update_period_6.json", @@ -456,6 +450,11 @@ mod tests { "finality_update_period_9.json", ]; + let mut store = MockStore::new( + bootstrap.beacon_header().clone(), + bootstrap.current_sync_committee().clone(), + execution_payload_state_root, + ); for update in updates.into_iter() { let (consensus_update, execution_update) = get_updates(format!("{}/{}", TEST_DATA_DIR, update)); @@ -463,7 +462,9 @@ mod tests { .validate_updates(&ctx, &store, &consensus_update, &execution_update) .is_ok()); let res = store.apply_light_client_update(&ctx, &consensus_update); - assert!(res.is_ok() && res.unwrap()); + assert!(res.is_ok(), "{:?}", res); + assert!(res.as_ref().unwrap().is_some()); + store = res.unwrap().unwrap(); } } @@ -510,7 +511,7 @@ mod tests { use crate::{ context::{Fraction, LightClientContext}, - misbehaviour::NextSyncCommitteeMisbehaviour, + misbehaviour::{FinalizedHeaderMisbehaviour, NextSyncCommitteeMisbehaviour}, mock::MockStore, }; use ethereum_consensus::{config, types::U64}; @@ -523,7 +524,6 @@ mod tests { #[test] fn test_lc() { let scm = MockSyncCommitteeManager::<32>::new(1, 4); - let current_sync_committee = scm.get_committee(1); let ctx = LightClientContext::new_with_config( config::minimal::get_config(), Default::default(), @@ -535,48 +535,168 @@ mod tests { .as_secs() .into(), ); - let start_slot = - U64(1) * ctx.slots_per_epoch() * ctx.epochs_per_sync_committee_period(); + let period_1 = U64(1) * ctx.slots_per_epoch() * ctx.epochs_per_sync_committee_period(); let initial_header = BeaconBlockHeader { - slot: start_slot, + slot: period_1, ..Default::default() }; + let current_sync_committee = scm.get_committee(1); let store = MockStore::new( initial_header, current_sync_committee.to_committee(), Default::default(), ); - + let base_signature_slot = period_1 + 11; + let base_attested_slot = base_signature_slot - 1; + let base_finalized_epoch = base_attested_slot / ctx.slots_per_epoch(); let dummy_execution_state_root = [1u8; 32].into(); let dummy_execution_block_number = 1; - let update_1 = gen_light_client_update::<32, _>( - &ctx, - start_slot + 11, - start_slot + 10, - U64(1) * ctx.slots_per_epoch(), - dummy_execution_state_root, - dummy_execution_block_number.into(), - &scm, - ); - assert!(SyncProtocolVerifier::default() - .validate_consensus_update(&ctx, &store, &update_1) - .is_ok()); - let update_2_insufficient_attestations = gen_light_client_update_with_params( - &ctx, - start_slot + 11, - start_slot + 10, - U64(1) * ctx.slots_per_epoch(), - dummy_execution_state_root, - dummy_execution_block_number.into(), - current_sync_committee, - scm.get_committee(3), - 21, - ); - assert!(SyncProtocolVerifier::default() - .validate_consensus_update(&ctx, &store, &update_2_insufficient_attestations) - .is_err()); + { + let update_valid = gen_light_client_update::<32, _>( + &ctx, + base_signature_slot, + base_attested_slot, + base_finalized_epoch, + dummy_execution_state_root, + dummy_execution_block_number.into(), + &scm, + ); + let res = SyncProtocolVerifier::default().validate_consensus_update( + &ctx, + &store, + &update_valid, + ); + assert!(res.is_ok(), "{:?}", res); + } + { + let update_insufficient_attestations = gen_light_client_update_with_params( + &ctx, + base_signature_slot, + base_attested_slot, + base_finalized_epoch, + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(3), + 21, + ); + let res = SyncProtocolVerifier::default().validate_consensus_update( + &ctx, + &store, + &update_insufficient_attestations, + ); + assert!(res.is_err(), "{:?}", res); + } + { + let update_zero_attestations = gen_light_client_update_with_params( + &ctx, + base_signature_slot, + base_attested_slot, + base_finalized_epoch, + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(3), + 0, + ); + let res = SyncProtocolVerifier::default().validate_consensus_update( + &ctx, + &store, + &update_zero_attestations, + ); + assert!(res.is_err(), "{:?}", res); + } + { + // + // | + // +-----------+ +----------+ | +-----------+ + // | finalized | <-- | attested | <-- | signature | + // +-----------+ +----------+ | +-----------+ + // | + // | + // sync committee + // period boundary + // + let next_period_signature_slot = base_signature_slot + + ctx.slots_per_epoch() * ctx.epochs_per_sync_committee_period(); + let update_unknown_next_committee = gen_light_client_update::<32, _>( + &ctx, + next_period_signature_slot, + base_attested_slot, + base_finalized_epoch, + dummy_execution_state_root, + dummy_execution_block_number.into(), + &scm, + ); + let res = SyncProtocolVerifier::default().validate_consensus_update( + &ctx, + &store, + &update_unknown_next_committee, + ); + assert!(res.is_err(), "{:?}", res); + + let store = MockStore { + next_sync_committee: Some(scm.get_committee(2).to_committee()), + ..store.clone() + }; + let update_valid = gen_light_client_update::<32, _>( + &ctx, + next_period_signature_slot, + base_attested_slot, + base_finalized_epoch, + dummy_execution_state_root, + dummy_execution_block_number.into(), + &scm, + ); + let res = SyncProtocolVerifier::default().validate_consensus_update( + &ctx, + &store, + &update_valid, + ); + assert!(res.is_ok(), "{:?}", res); + } + { + // + // | + // +-----------+ | +----------+ +-----------+ + // | finalized | <-- | attested | <-- | signature | + // +-----------+ | +----------+ +-----------+ + // | + // | + // sync committee + // period boundary + // + let next_period_signature_slot = base_signature_slot + + ctx.slots_per_epoch() * ctx.epochs_per_sync_committee_period(); + let next_period_attested_slot = next_period_signature_slot - 1; + let store = MockStore { + next_sync_committee: Some(scm.get_committee(2).to_committee()), + ..store.clone() + }; + let update_invalid_inconsistent_periods = gen_light_client_update::<32, _>( + &ctx, + next_period_signature_slot, + next_period_attested_slot, + base_finalized_epoch, + dummy_execution_state_root, + dummy_execution_block_number.into(), + &scm, + ); + let res = SyncProtocolVerifier::default().validate_consensus_update( + &ctx, + &store, + &update_invalid_inconsistent_periods, + ); + assert!(res.is_err(), "{:?}", res); + if let Some(Error::InconsistentUpdatePeriod(a, b)) = res.as_ref().err() { + assert_eq!(a, &1.into()); + assert_eq!(b, &2.into()); + } else { + panic!("unexpected error: {:?}", res); + } + } } #[test] @@ -610,12 +730,14 @@ mod tests { let dummy_execution_state_root = [1u8; 32].into(); let dummy_execution_block_number = 1; let base_signature_slot = start_slot + 11; - let base_attested_slot = start_slot + 10; + let base_attested_slot = base_signature_slot - 1; + let base_finalized_epoch = base_attested_slot / ctx.slots_per_epoch(); + let update_1 = gen_light_client_update_with_params::<32, _>( &ctx, base_signature_slot, base_attested_slot, - U64(1) * ctx.slots_per_epoch(), + base_finalized_epoch, dummy_execution_state_root, dummy_execution_block_number.into(), current_sync_committee, @@ -628,71 +750,149 @@ mod tests { &ctx, base_signature_slot, base_attested_slot, - U64(1) * ctx.slots_per_epoch(), + base_finalized_epoch, dummy_execution_state_root, dummy_execution_block_number.into(), current_sync_committee, scm.get_committee(3), 32, ); - assert!(SyncProtocolVerifier::default() - .validate_misbehaviour( - &ctx, - &store, - Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { - consensus_update_1: update_1.clone(), - consensus_update_2: update_valid, - }), - ) - .is_ok()); + let res = SyncProtocolVerifier::default().validate_misbehaviour( + &ctx, + &store, + Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_valid, + }), + ); + assert!(res.is_ok(), "{:?}", res); + } + { + let update_valid_different_slots = gen_light_client_update_with_params::<32, _>( + &ctx, + base_signature_slot + 1, + base_attested_slot + 1, + base_finalized_epoch, + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(3), + 32, + ); + let res = SyncProtocolVerifier::default().validate_misbehaviour( + &ctx, + &store, + Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_valid_different_slots, + }), + ); + assert!(res.is_ok(), "{:?}", res); } { let update_insufficient_attestations = gen_light_client_update_with_params::<32, _>( &ctx, base_signature_slot, base_attested_slot, - U64(1) * ctx.slots_per_epoch(), + base_finalized_epoch, dummy_execution_state_root, dummy_execution_block_number.into(), current_sync_committee, scm.get_committee(3), - 21, + 21, // at least 22 is required ); - assert!(SyncProtocolVerifier::default() - .validate_misbehaviour( - &ctx, - &store, - Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { - consensus_update_1: update_1.clone(), - consensus_update_2: update_insufficient_attestations, - },), - ) - .is_err()); + let res = SyncProtocolVerifier::default().validate_misbehaviour( + &ctx, + &store, + Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_insufficient_attestations, + }), + ); + assert!(res.is_err(), "{:?}", res); } { - let update_valid_different_slots = gen_light_client_update_with_params::<32, _>( + let different_period_attested_slot = base_attested_slot + + ctx.slots_per_epoch() * ctx.epochs_per_sync_committee_period(); + let update_different_attested_period = gen_light_client_update_with_params::<32, _>( &ctx, - base_signature_slot + 1, - base_attested_slot + 1, - U64(1) * ctx.slots_per_epoch(), + base_signature_slot, + different_period_attested_slot, + base_finalized_epoch, dummy_execution_state_root, dummy_execution_block_number.into(), current_sync_committee, scm.get_committee(3), 32, ); - assert!(SyncProtocolVerifier::default() - .validate_misbehaviour( - &ctx, - &store, - Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { - consensus_update_1: update_1.clone(), - consensus_update_2: update_valid_different_slots, - },), - ) - .is_ok()); + let res = SyncProtocolVerifier::default().validate_misbehaviour( + &ctx, + &store, + Misbehaviour::NextSyncCommittee(NextSyncCommitteeMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_different_attested_period, + }), + ); + assert!(res.is_err(), "{:?}", res); + } + { + let different_dummy_execution_state_root = [2u8; 32].into(); + let update_different_finalized_block = gen_light_client_update_with_params::<32, _>( + &ctx, + base_signature_slot, + base_attested_slot, + base_finalized_epoch, + different_dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(2), + 32, + ); + let res = SyncProtocolVerifier::default().validate_misbehaviour( + &ctx, + &store, + Misbehaviour::FinalizedHeader(FinalizedHeaderMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_different_finalized_block, + }), + ); + assert!(res.is_ok(), "{:?}", res); + } + { + let different_dummy_execution_state_root = [2u8; 32].into(); + let different_finalized_epoch = base_finalized_epoch - 1; + let update_different_finalized_block = gen_light_client_update_with_params::<32, _>( + &ctx, + base_signature_slot, + base_attested_slot, + different_finalized_epoch, + different_dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(2), + 32, + ); + let res = SyncProtocolVerifier::default().validate_misbehaviour( + &ctx, + &store, + Misbehaviour::FinalizedHeader(FinalizedHeaderMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_different_finalized_block, + }), + ); + assert!(res.is_err(), "{:?}", res); + } + { + let res = SyncProtocolVerifier::default().validate_misbehaviour( + &ctx, + &store, + Misbehaviour::FinalizedHeader(FinalizedHeaderMisbehaviour { + consensus_update_1: update_1.clone(), + consensus_update_2: update_1.clone(), + }), + ); + assert!(res.is_err(), "{:?}", res); } - {} } } @@ -842,6 +1042,7 @@ mod tests { scm: &MockSyncCommitteeManager, ) -> ConsensusUpdateInfo { let signature_period = compute_sync_committee_period_at_slot(ctx, signature_slot); + let attested_period = compute_sync_committee_period_at_slot(ctx, attested_slot); gen_light_client_update_with_params( ctx, signature_slot, @@ -850,7 +1051,7 @@ mod tests { execution_state_root, execution_block_number, scm.get_committee(signature_period.into()), - scm.get_committee((signature_period + 1).into()), + scm.get_committee((attested_period + 1).into()), SYNC_COMMITTEE_SIZE, ) } @@ -866,7 +1067,7 @@ mod tests { finalized_epoch: Epoch, execution_state_root: H256, execution_block_number: BlockNumber, - current_sync_committee: &MockSyncCommittee, + sync_committee: &MockSyncCommittee, next_sync_committee: &MockSyncCommittee, sign_num: usize, ) -> ConsensusUpdateInfo { @@ -887,7 +1088,7 @@ mod tests { ctx, attested_slot, finalized_root, - current_sync_committee.to_committee(), + sync_committee.to_committee(), next_sync_committee.to_committee(), ); @@ -907,7 +1108,7 @@ mod tests { attested_header: attested_header.clone(), finalized_header: (finalized_block.to_header(), finalized_checkpoint_branch), signature_slot, - sync_aggregate: current_sync_committee.sign_header( + sync_aggregate: sync_committee.sign_header( ctx, signature_slot, attested_header, diff --git a/crates/light-client-verifier/src/misbehaviour.rs b/crates/light-client-verifier/src/misbehaviour.rs index 92fc775..06f9f80 100644 --- a/crates/light-client-verifier/src/misbehaviour.rs +++ b/crates/light-client-verifier/src/misbehaviour.rs @@ -31,7 +31,7 @@ impl> /// FinalizedHeaderMisbehaviour is a misbehaviour that satisfies the followings: /// 1. Two updates are valid with the consensus state of the client -/// 2. Each finalized header in the two updates has a same finalized slot +/// 2. Each finalized header in the two updates has a same slot /// 3. The two finalized headers are different from each other #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct FinalizedHeaderMisbehaviour< diff --git a/crates/light-client-verifier/src/mock.rs b/crates/light-client-verifier/src/mock.rs index aeb6660..705e46e 100644 --- a/crates/light-client-verifier/src/mock.rs +++ b/crates/light-client-verifier/src/mock.rs @@ -39,10 +39,11 @@ impl MockStore { CC: ChainContext + ConsensusVerificationContext, CU: ConsensusUpdate, >( - &mut self, + &self, ctx: &CC, consensus_update: &CU, - ) -> Result { + ) -> Result, crate::errors::Error> { + let mut new_store = self.clone(); let store_period = compute_sync_committee_period_at_slot(ctx, self.latest_finalized_header.slot); let attested_period = compute_sync_committee_period_at_slot( @@ -50,18 +51,14 @@ impl MockStore { consensus_update.attested_beacon_header().slot, ); - let mut updated = if store_period == attested_period { + if store_period == attested_period { if let Some(committee) = consensus_update.next_sync_committee() { - self.next_sync_committee = Some(committee.clone()); - true - } else { - false + new_store.next_sync_committee = Some(committee.clone()); } } else if store_period + 1 == attested_period { if let Some(committee) = self.next_sync_committee.as_ref() { - self.current_sync_committee = committee.clone(); - self.next_sync_committee = consensus_update.next_sync_committee().cloned(); - true + new_store.current_sync_committee = committee.clone(); + new_store.next_sync_committee = consensus_update.next_sync_committee().cloned(); } else { return Err(crate::errors::Error::CannotRotateNextSyncCommittee( store_period, @@ -76,10 +73,13 @@ impl MockStore { )); }; if consensus_update.finalized_beacon_header().slot > self.latest_finalized_header.slot { - self.latest_finalized_header = consensus_update.finalized_beacon_header().clone(); - updated = true; + new_store.latest_finalized_header = consensus_update.finalized_beacon_header().clone(); + } + if self != &new_store { + Ok(Some(new_store)) + } else { + Ok(None) } - Ok(updated) } } @@ -100,12 +100,4 @@ impl LightClientStoreReader>( - &self, - ctx: &CC, - update: &CU, - ) -> Result<(), crate::errors::Error> { - update.ensure_consistent_update_period(ctx) - } } diff --git a/crates/light-client-verifier/src/state.rs b/crates/light-client-verifier/src/state.rs index b4722ac..b4b9d8c 100644 --- a/crates/light-client-verifier/src/state.rs +++ b/crates/light-client-verifier/src/state.rs @@ -19,5 +19,7 @@ pub trait LightClientStoreReader { &self, ctx: &CC, update: &C, - ) -> Result<(), Error>; + ) -> Result<(), Error> { + update.ensure_consistent_update_period(ctx) + } } From 52d0ac166b154bdeca64ccf2a6f866060189d667 Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Mon, 4 Nov 2024 20:50:53 +0900 Subject: [PATCH 4/5] add `test-utils` feature Signed-off-by: Jun Kimura --- crates/consensus/src/lib.rs | 4 + crates/light-client-verifier/Cargo.toml | 5 +- crates/light-client-verifier/src/consensus.rs | 855 +++++++++--------- 3 files changed, 435 insertions(+), 429 deletions(-) diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index de51fe8..0d166f5 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -20,3 +20,7 @@ pub mod merkle; pub mod preset; pub mod sync_protocol; pub mod types; + +/// re-export +pub use milagro_bls; +pub use ssz_rs; diff --git a/crates/light-client-verifier/Cargo.toml b/crates/light-client-verifier/Cargo.toml index 93965a6..0424c3c 100644 --- a/crates/light-client-verifier/Cargo.toml +++ b/crates/light-client-verifier/Cargo.toml @@ -13,12 +13,12 @@ patricia-merkle-trie = { git = "https://github.com/bluele/patricia-merkle-trie", primitive-types = { version = "0.12.1", default-features = false } rlp = { version = "0.5.2", default-features = false } +rand = { version = "0.8.5", features = ["std", "std_rng"], optional = true} + [dev-dependencies] serde_json = "1.0.91" hex-literal = "0.3.4" rand = { version = "0.8.5", features = ["std", "std_rng"] } -ssz-rs = { git = "https://github.com/bluele/ssz_rs", branch = "serde-no-std", default-features = false, features = ["serde"] } -milagro_bls = { git = "https://github.com/datachainlab/milagro_bls", rev = "bc2b5b5e8d48b7e2e1bfaa56dc2d93e13cb32095", default-features = false } [features] default = ["std"] @@ -27,3 +27,4 @@ std = [ "patricia-merkle-trie/std" ] mock = [] +test-utils = ["std", "mock", "rand"] diff --git a/crates/light-client-verifier/src/consensus.rs b/crates/light-client-verifier/src/consensus.rs index 8fc58dc..4ecbd07 100644 --- a/crates/light-client-verifier/src/consensus.rs +++ b/crates/light-client-verifier/src/consensus.rs @@ -344,6 +344,431 @@ pub fn verify_bls_signatures( } } +#[cfg(any(feature = "test-utils", test))] +pub mod test_utils { + use super::*; + use crate::updates::{ConsensusUpdateInfo, LightClientUpdate}; + use ethereum_consensus::milagro_bls::{ + AggregateSignature, PublicKey as BLSPublicKey, SecretKey as BLSSecretKey, + }; + use ethereum_consensus::ssz_rs::Vector; + use ethereum_consensus::{ + beacon::{BlockNumber, Checkpoint, Epoch, Slot}, + bls::{aggreate_public_key, PublicKey, Signature}, + fork::deneb, + merkle::MerkleTree, + preset::mainnet::DenebBeaconBlock, + sync_protocol::SyncAggregate, + types::U64, + }; + + #[derive(Clone)] + struct Validator { + sk: BLSSecretKey, + } + + impl Validator { + pub fn new() -> Self { + Self { + sk: BLSSecretKey::random(&mut rand::thread_rng()), + } + } + + pub fn sign(&self, msg: H256) -> BLSSignature { + BLSSignature::new(msg.as_bytes(), &self.sk) + } + + pub fn public_key(&self) -> BLSPublicKey { + BLSPublicKey::from_secret_key(&self.sk) + } + } + + #[derive(Clone)] + pub struct MockSyncCommittee { + committee: Vec, + } + + impl Default for MockSyncCommittee { + fn default() -> Self { + Self::new() + } + } + + impl MockSyncCommittee { + pub fn new() -> Self { + let mut committee = Vec::new(); + for _ in 0..SYNC_COMMITTEE_SIZE { + committee.push(Validator::new()); + } + Self { committee } + } + + pub fn to_committee(&self) -> SyncCommittee { + let mut pubkeys = Vec::new(); + for v in self.committee.iter() { + pubkeys.push(v.public_key()); + } + let aggregate_pubkey = aggreate_public_key(&pubkeys.to_vec()).unwrap(); + SyncCommittee { + pubkeys: Vector::from_iter(pubkeys.into_iter().map(PublicKey::from)), + aggregate_pubkey: PublicKey::from(aggregate_pubkey), + } + } + + pub fn sign_header( + &self, + ctx: &C, + signature_slot: U64, + attested_header: BeaconBlockHeader, + sign_num: usize, + ) -> SyncAggregate { + let fork_version_slot = signature_slot.max(1.into()) - 1; + let fork_version = + compute_fork_version(ctx, compute_epoch_at_slot(ctx, fork_version_slot)); + let domain = compute_domain( + ctx, + DOMAIN_SYNC_COMMITTEE, + Some(fork_version), + Some(ctx.genesis_validators_root()), + ) + .unwrap(); + let signing_root = compute_signing_root(attested_header, domain).unwrap(); + self.sign(signing_root, sign_num) + } + + pub fn sign( + &self, + signing_root: H256, + sign_num: usize, + ) -> SyncAggregate { + // let mut sigs = Vec::new(); + let mut agg_sig = AggregateSignature::new(); + let mut sg = SyncAggregate::::default(); + for (i, v) in self.committee.iter().enumerate() { + if i < sign_num { + agg_sig.add(&v.sign(signing_root)); + sg.sync_committee_bits.set(i, true); + } else { + sg.sync_committee_bits.set(i, false); + } + } + sg.sync_committee_signature = Signature::try_from(agg_sig.as_bytes().to_vec()).unwrap(); + sg + } + } + + pub struct MockSyncCommitteeManager { + pub base_period: u64, + pub committees: Vec>, + } + + impl MockSyncCommitteeManager { + pub fn new(base_period: u64, n_period: u64) -> Self { + let mut committees = Vec::new(); + for _ in 0..n_period { + committees.push(MockSyncCommittee::::new()); + } + Self { + base_period, + committees, + } + } + + pub fn get_committee(&self, period: u64) -> &MockSyncCommittee { + let idx = period - self.base_period; + &self.committees[idx as usize] + } + } + + pub fn gen_light_client_update< + const SYNC_COMMITTEE_SIZE: usize, + C: ChainConsensusVerificationContext, + >( + ctx: &C, + signature_slot: Slot, + attested_slot: Slot, + finalized_epoch: Epoch, + execution_state_root: H256, + execution_block_number: BlockNumber, + scm: &MockSyncCommitteeManager, + ) -> ConsensusUpdateInfo { + let signature_period = compute_sync_committee_period_at_slot(ctx, signature_slot); + let attested_period = compute_sync_committee_period_at_slot(ctx, attested_slot); + gen_light_client_update_with_params( + ctx, + signature_slot, + attested_slot, + finalized_epoch, + execution_state_root, + execution_block_number, + scm.get_committee(signature_period.into()), + scm.get_committee((attested_period + 1).into()), + SYNC_COMMITTEE_SIZE, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn gen_light_client_update_with_params< + const SYNC_COMMITTEE_SIZE: usize, + C: ChainConsensusVerificationContext, + >( + ctx: &C, + signature_slot: Slot, + attested_slot: Slot, + finalized_epoch: Epoch, + execution_state_root: H256, + execution_block_number: BlockNumber, + sync_committee: &MockSyncCommittee, + next_sync_committee: &MockSyncCommittee, + sign_num: usize, + ) -> ConsensusUpdateInfo { + assert!( + sign_num <= SYNC_COMMITTEE_SIZE, + "sign_num must be less than SYNC_COMMITTEE_SIZE({})", + SYNC_COMMITTEE_SIZE + ); + let finalized_block = gen_finalized_beacon_block::( + ctx, + finalized_epoch, + execution_state_root, + execution_block_number, + ); + let finalized_root = hash_tree_root(finalized_block.clone()).unwrap(); + let (attested_block, finalized_checkpoint_branch, _, next_sync_committee_branch) = + gen_attested_beacon_block( + ctx, + attested_slot, + finalized_root, + sync_committee.to_committee(), + next_sync_committee.to_committee(), + ); + + let (_, finalized_execution_branch) = + ethereum_consensus::fork::deneb::test_utils::gen_execution_payload_proof( + &finalized_block.body, + ) + .unwrap(); + let finalized_execution_root = + hash_tree_root(finalized_block.body.execution_payload.clone()) + .unwrap() + .0 + .into(); + + let attested_header = attested_block.to_header(); + let update = LightClientUpdate:: { + attested_header: attested_header.clone(), + finalized_header: (finalized_block.to_header(), finalized_checkpoint_branch), + signature_slot, + sync_aggregate: sync_committee.sign_header( + ctx, + signature_slot, + attested_header, + sign_num, + ), + next_sync_committee: Some(( + next_sync_committee.to_committee(), + next_sync_committee_branch, + )), + }; + + ConsensusUpdateInfo { + light_client_update: update, + finalized_execution_root, + finalized_execution_branch, + } + } + + fn compute_epoch_boundary_slot(ctx: &C, epoch: Epoch) -> Slot { + ctx.slots_per_epoch() * epoch + } + + pub fn gen_attested_beacon_block( + _: &C, + attested_slot: Slot, + finalized_header_root: H256, + current_sync_committee: SyncCommittee, + next_sync_committee: SyncCommittee, + ) -> (DenebBeaconBlock, Vec, Vec, Vec) { + let mut block = DenebBeaconBlock { + slot: attested_slot, + ..Default::default() + }; + + let finalized_checkpoint = Checkpoint { + root: finalized_header_root, + ..Default::default() + }; + let state = DummyDenebBeaconState::::new( + attested_slot.into(), + finalized_checkpoint, + current_sync_committee, + next_sync_committee, + ); + block.state_root = state.tree().root().unwrap().into(); + + let finalized_checkpoint_proof = state.generate_finalized_checkpoint(); + let current_sync_committee_proof = state.generate_current_sync_committee_proof(); + let next_sync_committee_proof = state.generate_next_sync_committee_proof(); + ( + block, + finalized_checkpoint_proof, + current_sync_committee_proof, + next_sync_committee_proof, + ) + } + + pub fn gen_finalized_beacon_block( + ctx: &C, + finalized_epoch: Epoch, + execution_state_root: H256, + execution_block_number: BlockNumber, + ) -> DenebBeaconBlock { + let mut block = DenebBeaconBlock { + slot: compute_epoch_boundary_slot(ctx, finalized_epoch), + ..Default::default() + }; + let mut body = deneb::BeaconBlockBody::default(); + body.execution_payload.state_root = execution_state_root; + body.execution_payload.block_number = execution_block_number; + block.body = body; + block + } + + pub type DummySSZType = [u8; 32]; + + /// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate + #[derive(Debug, Clone, Default)] + struct DummyDenebBeaconState { + genesis_time: DummySSZType, + genesis_validators_root: DummySSZType, + pub slot: U64, + fork: DummySSZType, + latest_block_header: DummySSZType, + block_roots: DummySSZType, + state_roots: DummySSZType, + historical_roots: DummySSZType, + eth1_data: DummySSZType, + eth1_data_votes: DummySSZType, + eth1_deposit_index: DummySSZType, + validators: DummySSZType, + balances: DummySSZType, + randao_mixes: DummySSZType, + slashings: DummySSZType, + previous_epoch_participation: DummySSZType, + current_epoch_participation: DummySSZType, + justification_bits: DummySSZType, + previous_justified_checkpoint: DummySSZType, + current_justified_checkpoint: DummySSZType, + pub finalized_checkpoint: Checkpoint, + inactivity_scores: DummySSZType, + pub current_sync_committee: SyncCommittee, + pub next_sync_committee: SyncCommittee, + latest_execution_payload_header: DummySSZType, + next_withdrawal_index: DummySSZType, + next_withdrawal_validator_index: DummySSZType, + historical_summaries: DummySSZType, + } + + impl DummyDenebBeaconState { + pub fn new( + slot: u64, + finalized_checkpoint: Checkpoint, + current_sync_committee: SyncCommittee, + next_sync_committee: SyncCommittee, + ) -> Self { + Self { + slot: slot.into(), + finalized_checkpoint, + current_sync_committee, + next_sync_committee, + ..Default::default() + } + } + + pub fn tree(&self) -> MerkleTree { + use ethereum_consensus::compute::hash_tree_root; + let tree = MerkleTree::from_leaves( + ([ + self.genesis_time, + self.genesis_validators_root, + hash_tree_root(self.slot).unwrap().0, + self.fork, + self.latest_block_header, + self.block_roots, + self.state_roots, + self.historical_roots, + self.eth1_data, + self.eth1_data_votes, + self.eth1_deposit_index, + self.validators, + self.balances, + self.randao_mixes, + self.slashings, + self.previous_epoch_participation, + self.current_epoch_participation, + self.justification_bits, + self.previous_justified_checkpoint, + self.current_justified_checkpoint, + hash_tree_root(self.finalized_checkpoint.clone()).unwrap().0, + self.inactivity_scores, + hash_tree_root(self.current_sync_committee.clone()) + .unwrap() + .0, + hash_tree_root(self.next_sync_committee.clone()).unwrap().0, + self.latest_execution_payload_header, + self.next_withdrawal_index, + self.next_withdrawal_validator_index, + self.historical_summaries, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ] as [_; 32]) + .as_ref(), + ); + tree + } + + pub fn generate_finalized_checkpoint(&self) -> Vec { + let br: Vec = self + .tree() + .proof(&[20]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect(); + let node = hash_tree_root(self.finalized_checkpoint.epoch) + .unwrap() + .0 + .into(); + let mut branch: Vec = Vec::new(); + branch.push(node); + for b in br.iter() { + branch.push(*b); + } + branch + } + + pub fn generate_current_sync_committee_proof(&self) -> Vec { + self.tree() + .proof(&[22]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect() + } + + pub fn generate_next_sync_committee_proof(&self) -> Vec { + self.tree() + .proof(&[23]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect() + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -507,20 +932,18 @@ mod tests { } mod deneb { - use std::time::SystemTime; - + use super::*; use crate::{ context::{Fraction, LightClientContext}, misbehaviour::{FinalizedHeaderMisbehaviour, NextSyncCommitteeMisbehaviour}, mock::MockStore, }; use ethereum_consensus::{config, types::U64}; - use utils::{ + use std::time::SystemTime; + use test_utils::{ gen_light_client_update, gen_light_client_update_with_params, MockSyncCommitteeManager, }; - use super::*; - #[test] fn test_lc() { let scm = MockSyncCommitteeManager::<32>::new(1, 4); @@ -895,426 +1318,4 @@ mod tests { } } } - - mod utils { - use crate::updates::{ConsensusUpdateInfo, LightClientUpdate}; - - use super::*; - use ethereum_consensus::{ - beacon::{BlockNumber, Checkpoint, Epoch, Slot}, - bls::{aggreate_public_key, PublicKey, Signature}, - fork::deneb, - merkle::MerkleTree, - preset::mainnet::DenebBeaconBlock, - sync_protocol::SyncAggregate, - types::U64, - }; - use milagro_bls::{ - AggregateSignature, PublicKey as BLSPublicKey, SecretKey as BLSSecretKey, - }; - use ssz_rs::Vector; - - #[derive(Clone)] - struct Validator { - sk: BLSSecretKey, - } - - impl Default for Validator { - fn default() -> Self { - Self { - sk: BLSSecretKey::random(&mut rand::thread_rng()), - } - } - } - - impl Validator { - pub fn sign(&self, msg: H256) -> BLSSignature { - BLSSignature::new(msg.as_bytes(), &self.sk) - } - - pub fn public_key(&self) -> BLSPublicKey { - BLSPublicKey::from_secret_key(&self.sk) - } - } - - #[derive(Clone)] - pub struct MockSyncCommittee { - committee: Vec, - } - - impl MockSyncCommittee { - pub fn new() -> Self { - let mut committee = Vec::new(); - for _ in 0..SYNC_COMMITTEE_SIZE { - committee.push(Validator::default()); - } - Self { committee } - } - - pub fn to_committee(&self) -> SyncCommittee { - let mut pubkeys = Vec::new(); - for v in self.committee.iter() { - pubkeys.push(v.public_key()); - } - let aggregate_pubkey = aggreate_public_key(&pubkeys.to_vec()).unwrap(); - SyncCommittee { - pubkeys: Vector::from_iter(pubkeys.into_iter().map(PublicKey::from)), - aggregate_pubkey: PublicKey::from(aggregate_pubkey), - } - } - - pub fn sign_header( - &self, - ctx: &C, - signature_slot: U64, - attested_header: BeaconBlockHeader, - sign_num: usize, - ) -> SyncAggregate { - let fork_version_slot = signature_slot.max(1.into()) - 1; - let fork_version = - compute_fork_version(ctx, compute_epoch_at_slot(ctx, fork_version_slot)); - let domain = compute_domain( - ctx, - DOMAIN_SYNC_COMMITTEE, - Some(fork_version), - Some(ctx.genesis_validators_root()), - ) - .unwrap(); - let signing_root = compute_signing_root(attested_header, domain).unwrap(); - self.sign(signing_root, sign_num) - } - - pub fn sign( - &self, - signing_root: H256, - sign_num: usize, - ) -> SyncAggregate { - // let mut sigs = Vec::new(); - let mut agg_sig = AggregateSignature::new(); - let mut sg = SyncAggregate::::default(); - for (i, v) in self.committee.iter().enumerate() { - if i < sign_num { - agg_sig.add(&v.sign(signing_root)); - sg.sync_committee_bits.set(i, true); - } else { - sg.sync_committee_bits.set(i, false); - } - } - sg.sync_committee_signature = - Signature::try_from(agg_sig.as_bytes().to_vec()).unwrap(); - sg - } - } - - pub struct MockSyncCommitteeManager { - pub base_period: u64, - pub committees: Vec>, - } - - impl MockSyncCommitteeManager { - pub fn new(base_period: u64, n_period: u64) -> Self { - let mut committees = Vec::new(); - for _ in 0..n_period { - committees.push(MockSyncCommittee::::new()); - } - Self { - base_period, - committees, - } - } - - pub fn get_committee(&self, period: u64) -> &MockSyncCommittee { - let idx = period - self.base_period; - &self.committees[idx as usize] - } - } - - pub fn gen_light_client_update< - const SYNC_COMMITTEE_SIZE: usize, - C: ChainConsensusVerificationContext, - >( - ctx: &C, - signature_slot: Slot, - attested_slot: Slot, - finalized_epoch: Epoch, - execution_state_root: H256, - execution_block_number: BlockNumber, - scm: &MockSyncCommitteeManager, - ) -> ConsensusUpdateInfo { - let signature_period = compute_sync_committee_period_at_slot(ctx, signature_slot); - let attested_period = compute_sync_committee_period_at_slot(ctx, attested_slot); - gen_light_client_update_with_params( - ctx, - signature_slot, - attested_slot, - finalized_epoch, - execution_state_root, - execution_block_number, - scm.get_committee(signature_period.into()), - scm.get_committee((attested_period + 1).into()), - SYNC_COMMITTEE_SIZE, - ) - } - - #[allow(clippy::too_many_arguments)] - pub fn gen_light_client_update_with_params< - const SYNC_COMMITTEE_SIZE: usize, - C: ChainConsensusVerificationContext, - >( - ctx: &C, - signature_slot: Slot, - attested_slot: Slot, - finalized_epoch: Epoch, - execution_state_root: H256, - execution_block_number: BlockNumber, - sync_committee: &MockSyncCommittee, - next_sync_committee: &MockSyncCommittee, - sign_num: usize, - ) -> ConsensusUpdateInfo { - assert!( - sign_num <= SYNC_COMMITTEE_SIZE, - "sign_num must be less than SYNC_COMMITTEE_SIZE({})", - SYNC_COMMITTEE_SIZE - ); - let finalized_block = gen_finalized_beacon_block::( - ctx, - finalized_epoch, - execution_state_root, - execution_block_number, - ); - let finalized_root = hash_tree_root(finalized_block.clone()).unwrap(); - let (attested_block, finalized_checkpoint_branch, _, next_sync_committee_branch) = - gen_attested_beacon_block( - ctx, - attested_slot, - finalized_root, - sync_committee.to_committee(), - next_sync_committee.to_committee(), - ); - - let (_, finalized_execution_branch) = - ethereum_consensus::fork::deneb::test_utils::gen_execution_payload_proof( - &finalized_block.body, - ) - .unwrap(); - let finalized_execution_root = - hash_tree_root(finalized_block.body.execution_payload.clone()) - .unwrap() - .0 - .into(); - - let attested_header = attested_block.to_header(); - let update = LightClientUpdate:: { - attested_header: attested_header.clone(), - finalized_header: (finalized_block.to_header(), finalized_checkpoint_branch), - signature_slot, - sync_aggregate: sync_committee.sign_header( - ctx, - signature_slot, - attested_header, - sign_num, - ), - next_sync_committee: Some(( - next_sync_committee.to_committee(), - next_sync_committee_branch, - )), - }; - - ConsensusUpdateInfo { - light_client_update: update, - finalized_execution_root, - finalized_execution_branch, - } - } - - fn compute_epoch_boundary_slot(ctx: &C, epoch: Epoch) -> Slot { - ctx.slots_per_epoch() * epoch - } - - pub fn gen_attested_beacon_block( - _: &C, - attested_slot: Slot, - finalized_header_root: H256, - current_sync_committee: SyncCommittee, - next_sync_committee: SyncCommittee, - ) -> (DenebBeaconBlock, Vec, Vec, Vec) { - let mut block = DenebBeaconBlock { - slot: attested_slot, - ..Default::default() - }; - - let finalized_checkpoint = Checkpoint { - root: finalized_header_root, - ..Default::default() - }; - let state = DummyDenebBeaconState::::new( - attested_slot.into(), - finalized_checkpoint, - current_sync_committee, - next_sync_committee, - ); - block.state_root = state.tree().root().unwrap().into(); - - let finalized_checkpoint_proof = state.generate_finalized_checkpoint(); - let current_sync_committee_proof = state.generate_current_sync_committee_proof(); - let next_sync_committee_proof = state.generate_next_sync_committee_proof(); - ( - block, - finalized_checkpoint_proof, - current_sync_committee_proof, - next_sync_committee_proof, - ) - } - - pub fn gen_finalized_beacon_block( - ctx: &C, - finalized_epoch: Epoch, - execution_state_root: H256, - execution_block_number: BlockNumber, - ) -> DenebBeaconBlock { - let mut block = DenebBeaconBlock { - slot: compute_epoch_boundary_slot(ctx, finalized_epoch), - ..Default::default() - }; - let mut body = deneb::BeaconBlockBody::default(); - body.execution_payload.state_root = execution_state_root; - body.execution_payload.block_number = execution_block_number; - block.body = body; - block - } - - pub type DummySSZType = [u8; 32]; - - /// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate - #[derive(Debug, Clone, Default)] - struct DummyDenebBeaconState { - genesis_time: DummySSZType, - genesis_validators_root: DummySSZType, - pub slot: U64, - fork: DummySSZType, - latest_block_header: DummySSZType, - block_roots: DummySSZType, - state_roots: DummySSZType, - historical_roots: DummySSZType, - eth1_data: DummySSZType, - eth1_data_votes: DummySSZType, - eth1_deposit_index: DummySSZType, - validators: DummySSZType, - balances: DummySSZType, - randao_mixes: DummySSZType, - slashings: DummySSZType, - previous_epoch_participation: DummySSZType, - current_epoch_participation: DummySSZType, - justification_bits: DummySSZType, - previous_justified_checkpoint: DummySSZType, - current_justified_checkpoint: DummySSZType, - pub finalized_checkpoint: Checkpoint, - inactivity_scores: DummySSZType, - pub current_sync_committee: SyncCommittee, - pub next_sync_committee: SyncCommittee, - latest_execution_payload_header: DummySSZType, - next_withdrawal_index: DummySSZType, - next_withdrawal_validator_index: DummySSZType, - historical_summaries: DummySSZType, - } - - impl DummyDenebBeaconState { - pub fn new( - slot: u64, - finalized_checkpoint: Checkpoint, - current_sync_committee: SyncCommittee, - next_sync_committee: SyncCommittee, - ) -> Self { - Self { - slot: slot.into(), - finalized_checkpoint, - current_sync_committee, - next_sync_committee, - ..Default::default() - } - } - - pub fn tree(&self) -> MerkleTree { - use ethereum_consensus::compute::hash_tree_root; - let tree = MerkleTree::from_leaves( - ([ - self.genesis_time, - self.genesis_validators_root, - hash_tree_root(self.slot).unwrap().0, - self.fork, - self.latest_block_header, - self.block_roots, - self.state_roots, - self.historical_roots, - self.eth1_data, - self.eth1_data_votes, - self.eth1_deposit_index, - self.validators, - self.balances, - self.randao_mixes, - self.slashings, - self.previous_epoch_participation, - self.current_epoch_participation, - self.justification_bits, - self.previous_justified_checkpoint, - self.current_justified_checkpoint, - hash_tree_root(self.finalized_checkpoint.clone()).unwrap().0, - self.inactivity_scores, - hash_tree_root(self.current_sync_committee.clone()) - .unwrap() - .0, - hash_tree_root(self.next_sync_committee.clone()).unwrap().0, - self.latest_execution_payload_header, - self.next_withdrawal_index, - self.next_withdrawal_validator_index, - self.historical_summaries, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ] as [_; 32]) - .as_ref(), - ); - tree - } - - pub fn generate_finalized_checkpoint(&self) -> Vec { - let br: Vec = self - .tree() - .proof(&[20]) - .proof_hashes() - .iter() - .map(|h| H256::from_slice(h)) - .collect(); - let node = hash_tree_root(self.finalized_checkpoint.epoch) - .unwrap() - .0 - .into(); - let mut branch: Vec = Vec::new(); - branch.push(node); - for b in br.iter() { - branch.push(*b); - } - branch - } - - pub fn generate_current_sync_committee_proof(&self) -> Vec { - self.tree() - .proof(&[22]) - .proof_hashes() - .iter() - .map(|h| H256::from_slice(h)) - .collect() - } - - pub fn generate_next_sync_committee_proof(&self) -> Vec { - self.tree() - .proof(&[23]) - .proof_hashes() - .iter() - .map(|h| H256::from_slice(h)) - .collect() - } - } - } } From 7621de9164c47feaf2f7cf7c29daf5cb7cc22505 Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Mon, 4 Nov 2024 23:55:20 +0900 Subject: [PATCH 5/5] add `prover` feature to `light-client-verifier` Signed-off-by: Jun Kimura --- crates/consensus/Cargo.toml | 4 +- crates/consensus/src/fork/bellatrix.rs | 87 +++++++------ crates/consensus/src/fork/capella.rs | 87 +++++++------ crates/consensus/src/fork/deneb.rs | 122 +++++++++--------- crates/consensus/src/merkle.rs | 1 + crates/light-client-cli/Cargo.toml | 2 +- crates/light-client-cli/src/client.rs | 4 +- crates/light-client-verifier/Cargo.toml | 2 +- crates/light-client-verifier/src/consensus.rs | 2 +- 9 files changed, 162 insertions(+), 149 deletions(-) diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index a6767d4..52469af 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -12,12 +12,14 @@ hex = { version = "0.4.3", default-features = false, features = ["alloc", "serde milagro_bls = { git = "https://github.com/datachainlab/milagro_bls", rev = "bc2b5b5e8d48b7e2e1bfaa56dc2d93e13cb32095", default-features = false } ssz-rs = { git = "https://github.com/bluele/ssz_rs", branch = "serde-no-std", default-features = false, features = ["serde"] } ssz-rs-derive = { git = "https://github.com/bluele/ssz_rs", branch = "serde-no-std", default-features = false } -rs_merkle = { version = "1.2.0", default-features = false } +rs_merkle = { version = "1.2.0", default-features = false, optional = true} primitive-types = { version = "0.12.1", default-features = false } [dev-dependencies] serde_json = "1.0.91" +rs_merkle = { version = "1.2.0", default-features = false } [features] default = ["std"] std = [] +prover = ["rs_merkle"] diff --git a/crates/consensus/src/fork/bellatrix.rs b/crates/consensus/src/fork/bellatrix.rs index 1c0436a..e39b756 100644 --- a/crates/consensus/src/fork/bellatrix.rs +++ b/crates/consensus/src/fork/bellatrix.rs @@ -6,9 +6,7 @@ use crate::{ }, bls::Signature, compute::hash_tree_root, - errors::Error, internal_prelude::*, - merkle::MerkleTree, sync_protocol::{SyncAggregate, SyncCommittee}, types::{Address, ByteList, ByteVector, Bytes32, H256, U256, U64}, }; @@ -259,49 +257,56 @@ pub struct LightClientUpdate { pub signature_slot: Slot, } -pub fn gen_execution_payload_field_proof< - const BYTES_PER_LOGS_BLOOM: usize, - const MAX_EXTRA_DATA_BYTES: usize, ->( - payload: &ExecutionPayloadHeader, - subtree_index: usize, -) -> Result<(Root, Vec), Error> { - let tree = MerkleTree::from_leaves( - ([ - payload.parent_hash.0, - hash_tree_root(payload.fee_recipient.clone()).unwrap().0, - payload.state_root.0, - payload.receipts_root.0, - hash_tree_root(payload.logs_bloom.clone()).unwrap().0, - payload.prev_randao.0, - hash_tree_root(payload.block_number).unwrap().0, - hash_tree_root(payload.gas_limit).unwrap().0, - hash_tree_root(payload.gas_used).unwrap().0, - hash_tree_root(payload.timestamp).unwrap().0, - hash_tree_root(payload.extra_data.clone()).unwrap().0, - hash_tree_root(payload.base_fee_per_gas.clone()).unwrap().0, - payload.block_hash.0, - payload.transactions_root.0, - Default::default(), - Default::default(), - ] as [_; 16]) - .as_ref(), - ); - Ok(( - H256(tree.root().unwrap()), - tree.proof(&[subtree_index]) - .proof_hashes() - .iter() - .map(|h| H256::from_slice(h)) - .collect::>(), - )) +#[cfg(any(feature = "prover", test))] +pub mod prover { + use super::*; + use crate::{errors::Error, merkle::MerkleTree}; + + pub fn gen_execution_payload_field_proof< + const BYTES_PER_LOGS_BLOOM: usize, + const MAX_EXTRA_DATA_BYTES: usize, + >( + payload: &ExecutionPayloadHeader, + subtree_index: usize, + ) -> Result<(Root, Vec), Error> { + let tree = MerkleTree::from_leaves( + ([ + payload.parent_hash.0, + hash_tree_root(payload.fee_recipient.clone()).unwrap().0, + payload.state_root.0, + payload.receipts_root.0, + hash_tree_root(payload.logs_bloom.clone()).unwrap().0, + payload.prev_randao.0, + hash_tree_root(payload.block_number).unwrap().0, + hash_tree_root(payload.gas_limit).unwrap().0, + hash_tree_root(payload.gas_used).unwrap().0, + hash_tree_root(payload.timestamp).unwrap().0, + hash_tree_root(payload.extra_data.clone()).unwrap().0, + hash_tree_root(payload.base_fee_per_gas.clone()).unwrap().0, + payload.block_hash.0, + payload.transactions_root.0, + Default::default(), + Default::default(), + ] as [_; 16]) + .as_ref(), + ); + Ok(( + H256(tree.root().unwrap()), + tree.proof(&[subtree_index]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect::>(), + )) + } } #[cfg(test)] mod tests { use super::*; + use crate::errors::Error; use crate::fork::bellatrix::{LightClientUpdate, BELLATRIX_FORK_SPEC}; - use crate::merkle::{get_subtree_index, is_valid_normalized_merkle_branch}; + use crate::merkle::{get_subtree_index, is_valid_normalized_merkle_branch, MerkleTree}; use crate::sync_protocol::SyncCommittee; use crate::{ beacon::DOMAIN_SYNC_COMMITTEE, @@ -354,7 +359,7 @@ mod tests { .is_ok()); { - let (root, proof) = gen_execution_payload_field_proof( + let (root, proof) = prover::gen_execution_payload_field_proof( &payload_header, get_subtree_index(BELLATRIX_FORK_SPEC.execution_payload_state_root_gindex) as usize, ) @@ -369,7 +374,7 @@ mod tests { .is_ok()); } { - let (root, proof) = gen_execution_payload_field_proof( + let (root, proof) = prover::gen_execution_payload_field_proof( &payload_header, get_subtree_index(BELLATRIX_FORK_SPEC.execution_payload_block_number_gindex) as usize, diff --git a/crates/consensus/src/fork/capella.rs b/crates/consensus/src/fork/capella.rs index 9c2e971..a58cf8c 100644 --- a/crates/consensus/src/fork/capella.rs +++ b/crates/consensus/src/fork/capella.rs @@ -7,9 +7,7 @@ use crate::{ }, bls::Signature, compute::hash_tree_root, - errors::Error, internal_prelude::*, - merkle::MerkleTree, sync_protocol::{SyncAggregate, SyncCommittee}, types::{Address, ByteList, ByteVector, Bytes32, H256, U256, U64}, }; @@ -293,49 +291,56 @@ pub struct LightClientHeader, } -pub fn gen_execution_payload_field_proof< - const BYTES_PER_LOGS_BLOOM: usize, - const MAX_EXTRA_DATA_BYTES: usize, ->( - payload: &ExecutionPayloadHeader, - leaf_index: usize, -) -> Result<(Root, Vec), Error> { - let tree = MerkleTree::from_leaves( - ([ - payload.parent_hash.0, - hash_tree_root(payload.fee_recipient.clone()).unwrap().0, - payload.state_root.0, - payload.receipts_root.0, - hash_tree_root(payload.logs_bloom.clone()).unwrap().0, - payload.prev_randao.0, - hash_tree_root(payload.block_number).unwrap().0, - hash_tree_root(payload.gas_limit).unwrap().0, - hash_tree_root(payload.gas_used).unwrap().0, - hash_tree_root(payload.timestamp).unwrap().0, - hash_tree_root(payload.extra_data.clone()).unwrap().0, - hash_tree_root(payload.base_fee_per_gas.clone()).unwrap().0, - payload.block_hash.0, - payload.transactions_root.0, - payload.withdrawals_root.0, - Default::default(), - ] as [_; 16]) - .as_ref(), - ); - Ok(( - H256(tree.root().unwrap()), - tree.proof(&[leaf_index]) - .proof_hashes() - .iter() - .map(|h| H256::from_slice(h)) - .collect::>(), - )) +#[cfg(any(feature = "prover", test))] +pub mod prover { + use super::*; + use crate::{errors::Error, merkle::MerkleTree}; + + pub fn gen_execution_payload_field_proof< + const BYTES_PER_LOGS_BLOOM: usize, + const MAX_EXTRA_DATA_BYTES: usize, + >( + payload: &ExecutionPayloadHeader, + leaf_index: usize, + ) -> Result<(Root, Vec), Error> { + let tree = MerkleTree::from_leaves( + ([ + payload.parent_hash.0, + hash_tree_root(payload.fee_recipient.clone()).unwrap().0, + payload.state_root.0, + payload.receipts_root.0, + hash_tree_root(payload.logs_bloom.clone()).unwrap().0, + payload.prev_randao.0, + hash_tree_root(payload.block_number).unwrap().0, + hash_tree_root(payload.gas_limit).unwrap().0, + hash_tree_root(payload.gas_used).unwrap().0, + hash_tree_root(payload.timestamp).unwrap().0, + hash_tree_root(payload.extra_data.clone()).unwrap().0, + hash_tree_root(payload.base_fee_per_gas.clone()).unwrap().0, + payload.block_hash.0, + payload.transactions_root.0, + payload.withdrawals_root.0, + Default::default(), + ] as [_; 16]) + .as_ref(), + ); + Ok(( + H256(tree.root().unwrap()), + tree.proof(&[leaf_index]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect::>(), + )) + } } #[cfg(test)] mod tests { use super::*; - use crate::merkle::{get_subtree_index, is_valid_normalized_merkle_branch}; + use crate::merkle::{get_subtree_index, is_valid_normalized_merkle_branch, MerkleTree}; use crate::{compute::hash_tree_root, types::H256}; + use rs_merkle::Error; use ssz_rs::Merkleized; use std::fs; @@ -375,7 +380,7 @@ mod tests { .is_ok()); { - let (root, proof) = gen_execution_payload_field_proof( + let (root, proof) = prover::gen_execution_payload_field_proof( &payload_header, get_subtree_index(CAPELLA_FORK_SPEC.execution_payload_state_root_gindex) as usize, ) @@ -390,7 +395,7 @@ mod tests { .is_ok()); } { - let (root, proof) = gen_execution_payload_field_proof( + let (root, proof) = prover::gen_execution_payload_field_proof( &payload_header, get_subtree_index(CAPELLA_FORK_SPEC.execution_payload_block_number_gindex) as usize, ) diff --git a/crates/consensus/src/fork/deneb.rs b/crates/consensus/src/fork/deneb.rs index 985dd6d..032cbed 100644 --- a/crates/consensus/src/fork/deneb.rs +++ b/crates/consensus/src/fork/deneb.rs @@ -7,9 +7,7 @@ use crate::{ }, bls::Signature, compute::hash_tree_root, - errors::Error, internal_prelude::*, - merkle::{get_subtree_index, MerkleTree}, sync_protocol::{SyncAggregate, SyncCommittee}, types::{Address, ByteList, ByteVector, Bytes32, H256, U256, U64}, }; @@ -311,62 +309,65 @@ pub struct LightClientHeader, } -pub fn gen_execution_payload_field_proof< - const BYTES_PER_LOGS_BLOOM: usize, - const MAX_EXTRA_DATA_BYTES: usize, ->( - payload: &ExecutionPayloadHeader, - leaf_index: usize, -) -> Result<(Root, Vec), Error> { - let tree = MerkleTree::from_leaves( - ([ - payload.parent_hash.0, - hash_tree_root(payload.fee_recipient.clone()).unwrap().0, - payload.state_root.0, - payload.receipts_root.0, - hash_tree_root(payload.logs_bloom.clone()).unwrap().0, - payload.prev_randao.0, - hash_tree_root(payload.block_number).unwrap().0, - hash_tree_root(payload.gas_limit).unwrap().0, - hash_tree_root(payload.gas_used).unwrap().0, - hash_tree_root(payload.timestamp).unwrap().0, - hash_tree_root(payload.extra_data.clone()).unwrap().0, - hash_tree_root(payload.base_fee_per_gas.clone()).unwrap().0, - payload.block_hash.0, - payload.transactions_root.0, - payload.withdrawals_root.0, - hash_tree_root(payload.blob_gas_used).unwrap().0, - hash_tree_root(payload.excess_blob_gas).unwrap().0, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ] as [_; 32]) - .as_ref(), - ); - Ok(( - H256(tree.root().unwrap()), - tree.proof(&[leaf_index]) - .proof_hashes() - .iter() - .map(|h| H256::from_slice(h)) - .collect::>(), - )) -} - -pub mod test_utils { +#[cfg(any(feature = "prover", test))] +pub mod prover { use super::*; + use crate::errors::Error; + use crate::merkle::{get_subtree_index, MerkleTree}; + + pub fn gen_execution_payload_field_proof< + const BYTES_PER_LOGS_BLOOM: usize, + const MAX_EXTRA_DATA_BYTES: usize, + >( + payload: &ExecutionPayloadHeader, + leaf_index: usize, + ) -> Result<(Root, Vec), Error> { + let tree = MerkleTree::from_leaves( + ([ + payload.parent_hash.0, + hash_tree_root(payload.fee_recipient.clone()).unwrap().0, + payload.state_root.0, + payload.receipts_root.0, + hash_tree_root(payload.logs_bloom.clone()).unwrap().0, + payload.prev_randao.0, + hash_tree_root(payload.block_number).unwrap().0, + hash_tree_root(payload.gas_limit).unwrap().0, + hash_tree_root(payload.gas_used).unwrap().0, + hash_tree_root(payload.timestamp).unwrap().0, + hash_tree_root(payload.extra_data.clone()).unwrap().0, + hash_tree_root(payload.base_fee_per_gas.clone()).unwrap().0, + payload.block_hash.0, + payload.transactions_root.0, + payload.withdrawals_root.0, + hash_tree_root(payload.blob_gas_used).unwrap().0, + hash_tree_root(payload.excess_blob_gas).unwrap().0, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ] as [_; 32]) + .as_ref(), + ); + Ok(( + H256(tree.root().unwrap()), + tree.proof(&[leaf_index]) + .proof_hashes() + .iter() + .map(|h| H256::from_slice(h)) + .collect::>(), + )) + } pub fn gen_execution_payload_proof< const MAX_PROPOSER_SLASHINGS: usize, @@ -463,8 +464,7 @@ mod tests { block.hash_tree_root().unwrap() ); - let (block_root, payload_proof) = - test_utils::gen_execution_payload_proof(&block.body).unwrap(); + let (block_root, payload_proof) = prover::gen_execution_payload_proof(&block.body).unwrap(); assert_eq!( block_root.as_bytes(), block.body.hash_tree_root().unwrap().as_bytes() @@ -482,7 +482,7 @@ mod tests { .is_ok()); { - let (root, proof) = gen_execution_payload_field_proof( + let (root, proof) = prover::gen_execution_payload_field_proof( &payload_header, get_subtree_index(DENEB_FORK_SPEC.execution_payload_state_root_gindex) as usize, ) @@ -497,7 +497,7 @@ mod tests { .is_ok()); } { - let (root, proof) = gen_execution_payload_field_proof( + let (root, proof) = prover::gen_execution_payload_field_proof( &payload_header, get_subtree_index(DENEB_FORK_SPEC.execution_payload_block_number_gindex) as usize, ) diff --git a/crates/consensus/src/merkle.rs b/crates/consensus/src/merkle.rs index 49ff6d3..2b68b6f 100644 --- a/crates/consensus/src/merkle.rs +++ b/crates/consensus/src/merkle.rs @@ -2,6 +2,7 @@ use crate::{beacon::Root, errors::MerkleError, internal_prelude::*, types::H256} use sha2::{Digest, Sha256}; /// MerkleTree is a merkle tree implementation using sha256 as a hashing algorithm. +#[cfg(any(feature = "prover", test))] pub type MerkleTree = rs_merkle::MerkleTree; pub fn is_valid_normalized_merkle_branch( diff --git a/crates/light-client-cli/Cargo.toml b/crates/light-client-cli/Cargo.toml index f7562dc..369a8ff 100644 --- a/crates/light-client-cli/Cargo.toml +++ b/crates/light-client-cli/Cargo.toml @@ -18,6 +18,6 @@ dirs = "4.0" env_logger = { version = "0.10.0" } tokio = { version = "1.24.1", default-features = false, features = ["rt-multi-thread", "macros"] } -ethereum-consensus = { path = "../consensus" } +ethereum-consensus = { path = "../consensus", features = ["prover"]} ethereum-light-client-verifier = { path = "../light-client-verifier" } lodestar-rpc = { path = "../lodestar-rpc" } diff --git a/crates/light-client-cli/src/client.rs b/crates/light-client-cli/src/client.rs index ffb0358..e7aaae0 100644 --- a/crates/light-client-cli/src/client.rs +++ b/crates/light-client-cli/src/client.rs @@ -224,11 +224,11 @@ impl< let execution_update = { let execution_payload_header = update.finalized_header.execution.clone(); - let (_, state_root_branch) = deneb::gen_execution_payload_field_proof( + let (_, state_root_branch) = deneb::prover::gen_execution_payload_field_proof( &execution_payload_header, EXECUTION_PAYLOAD_STATE_ROOT_SUBTREE_INDEX, )?; - let (_, block_number_branch) = deneb::gen_execution_payload_field_proof( + let (_, block_number_branch) = deneb::prover::gen_execution_payload_field_proof( &execution_payload_header, EXECUTION_PAYLOAD_BLOCK_NUMBER_SUBTREE_INDEX, )?; diff --git a/crates/light-client-verifier/Cargo.toml b/crates/light-client-verifier/Cargo.toml index 0424c3c..ce679df 100644 --- a/crates/light-client-verifier/Cargo.toml +++ b/crates/light-client-verifier/Cargo.toml @@ -27,4 +27,4 @@ std = [ "patricia-merkle-trie/std" ] mock = [] -test-utils = ["std", "mock", "rand"] +test-utils = ["std", "mock", "rand", "ethereum-consensus/prover"] diff --git a/crates/light-client-verifier/src/consensus.rs b/crates/light-client-verifier/src/consensus.rs index 4ecbd07..8025400 100644 --- a/crates/light-client-verifier/src/consensus.rs +++ b/crates/light-client-verifier/src/consensus.rs @@ -544,7 +544,7 @@ pub mod test_utils { ); let (_, finalized_execution_branch) = - ethereum_consensus::fork::deneb::test_utils::gen_execution_payload_proof( + ethereum_consensus::fork::deneb::prover::gen_execution_payload_proof( &finalized_block.body, ) .unwrap();