From c0baa16db941130f70639d6a88f37bcea6e188cf Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Sat, 2 Nov 2024 17:26:48 +0900 Subject: [PATCH] add tests for misbehaviour case Signed-off-by: Jun Kimura --- crates/consensus/src/errors.rs | 4 +- crates/consensus/src/fork/deneb.rs | 136 +-- 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 | 914 +++++++++++++++--- .../light-client-verifier/src/misbehaviour.rs | 9 +- crates/light-client-verifier/src/updates.rs | 11 + 17 files changed, 853 insertions(+), 225 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..29e7454 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}, }; @@ -442,72 +442,72 @@ 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(), - )) - } +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(), + )) } 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..c212acf 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,765 @@ 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 mut initial_header = BeaconBlockHeader::default(); + initial_header.slot = start_slot; + 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).into(), + (start_slot + 10).into(), + 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).into(), + (start_slot + 10).into(), + 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 mut initial_header = BeaconBlockHeader::default(); + initial_header.slot = start_slot; + 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).into(); + let base_attested_slot = (start_slot + 10).into(); + 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(); + let mut committee = SyncCommittee::::default(); + committee.pubkeys = + Vector::from_iter(pubkeys.into_iter().map(|pk| PublicKey::from(pk))); + committee.aggregate_pubkey = PublicKey::from(aggregate_pubkey); + committee + } + + 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, ) - .unwrap(), - min_genesis_time: U64(1578009600), + } + + 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 mut update = LightClientUpdate::default(); + update.attested_header = attested_block.to_header(); + update.finalized_header.0 = finalized_block.to_header(); + update.finalized_header.1 = finalized_checkpoint_branch; + update.signature_slot = signature_slot; + update.sync_aggregate = current_sync_committee.sign_header( + ctx, + signature_slot, + update.attested_header.clone(), + sign_num, + ); + update.next_sync_committee = Some(( + next_sync_committee.to_committee(), + next_sync_committee_branch, + )); + // update + + 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::default(); + block.slot = attested_slot; + + 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::default(); + block.slot = compute_epoch_boundary_slot(ctx, finalized_epoch); + 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.clone()) + .unwrap() + .0 + .into(); + let mut branch: Vec = Vec::new(); + branch.push(node); + for b in br.iter() { + branch.push(b.clone()); + } + 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;