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/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/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 319f5a7..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::MerkleTree, sync_protocol::{SyncAggregate, SyncCommittee}, types::{Address, ByteList, ByteVector, Bytes32, H256, U256, U64}, }; @@ -311,58 +309,133 @@ 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::>(), - )) +#[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, + 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)] @@ -391,7 +464,7 @@ mod tests { block.hash_tree_root().unwrap() ); - let (block_root, payload_proof) = 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() @@ -409,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, ) @@ -424,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, ) @@ -442,72 +515,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/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/consensus/src/merkle.rs b/crates/consensus/src/merkle.rs index 7e34fa7..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( @@ -62,6 +63,7 @@ pub fn is_valid_merkle_branch( branch.to_vec(), subtree_index, root, + value, )) } } 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 b1d7134..e7aaae0 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; @@ -225,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, )?; @@ -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/Cargo.toml b/crates/light-client-verifier/Cargo.toml index 9b1f662..ce679df 100644 --- a/crates/light-client-verifier/Cargo.toml +++ b/crates/light-client-verifier/Cargo.toml @@ -13,9 +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"] } [features] default = ["std"] @@ -24,3 +27,4 @@ std = [ "patricia-merkle-trie/std" ] mock = [] +test-utils = ["std", "mock", "rand", "ethereum-consensus/prover"] 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..8025400 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)?; + 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)?; 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, ) @@ -143,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, @@ -206,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(), )) } } @@ -247,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 @@ -359,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(), )); } @@ -397,159 +344,978 @@ pub fn verify_bls_signatures( } } -#[cfg(test)] -mod tests_bellatrix { +#[cfg(any(feature = "test-utils", test))] +pub mod test_utils { use super::*; - use crate::{ - context::{Fraction, LightClientContext}, - mock::MockStore, - updates::{ - bellatrix::{ConsensusUpdateInfo, ExecutionUpdateInfo, LightClientBootstrapInfo}, - LightClientBootstrap, - }, + 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::Version, - bls::aggreate_public_key, - config::{minimal, Config}, - fork::{ - altair::ALTAIR_FORK_SPEC, bellatrix::BELLATRIX_FORK_SPEC, ForkParameter, ForkParameters, - }, - preset, + beacon::{BlockNumber, Checkpoint, Epoch, Slot}, + bls::{aggreate_public_key, PublicKey, Signature}, + fork::deneb, + merkle::MerkleTree, + preset::mainnet::DenebBeaconBlock, + sync_protocol::SyncAggregate, 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()); + + #[derive(Clone)] + struct Validator { + sk: BLSSecretKey, } - #[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()); + 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!( - pubkey - == bootstrap - .current_sync_committee() - .aggregate_pubkey - .clone() - .try_into() - .unwrap() + 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::prover::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, + } } - #[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()); + 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 mut store = MockStore::new( - bootstrap.beacon_header().clone(), - bootstrap.current_sync_committee().clone(), - execution_payload_state_root, + 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 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()); - } + 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, + ) } - // 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() + 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 } - 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() + 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, } - 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), + 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::*; + + mod bellatrix { + 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/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 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", + ]; + + 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)); + 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); + assert!(res.as_ref().unwrap().is_some()); + store = res.unwrap().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), + } + } + } + + mod deneb { + use super::*; + use crate::{ + context::{Fraction, LightClientContext}, + misbehaviour::{FinalizedHeaderMisbehaviour, NextSyncCommitteeMisbehaviour}, + mock::MockStore, + }; + use ethereum_consensus::{config, types::U64}; + use std::time::SystemTime; + use test_utils::{ + gen_light_client_update, gen_light_client_update_with_params, MockSyncCommitteeManager, + }; + + #[test] + fn test_lc() { + let scm = MockSyncCommitteeManager::<32>::new(1, 4); + 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 period_1 = U64(1) * ctx.slots_per_epoch() * ctx.epochs_per_sync_committee_period(); + + let initial_header = BeaconBlockHeader { + 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_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] + 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(), + ); + + 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 = 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, + base_finalized_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, + 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, + }), + ); + 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, + base_finalized_epoch, + dummy_execution_state_root, + dummy_execution_block_number.into(), + current_sync_committee, + scm.get_committee(3), + 21, // at least 22 is required + ); + 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 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, + different_period_attested_slot, + 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_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); + } } } } 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/misbehaviour.rs b/crates/light-client-verifier/src/misbehaviour.rs index d35061d..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< @@ -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/mock.rs b/crates/light-client-verifier/src/mock.rs index 771913c..705e46e 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,45 +31,73 @@ 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, >( - &mut self, + &self, 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; - } + ) -> 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( + ctx, + consensus_update.attested_beacon_header().slot, + ); + + if store_period == attested_period { + if let Some(committee) = consensus_update.next_sync_committee() { + 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() { + 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, + 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; + new_store.latest_finalized_header = consensus_update.finalized_beacon_header().clone(); + } + if self != &new_store { + Ok(Some(new_store)) + } else { + Ok(None) } - Ok(updated) } } 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 next_sync_committee(&self) -> Option<&SyncCommittee> { - self.next_sync_committee.as_ref() + 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 + } } } diff --git a/crates/light-client-verifier/src/state.rs b/crates/light-client-verifier/src/state.rs index d95cf03..b4b9d8c 100644 --- a/crates/light-client-verifier/src/state.rs +++ b/crates/light-client-verifier/src/state.rs @@ -1,71 +1,25 @@ 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> { + update.ensure_consistent_update_period(ctx) } } diff --git a/crates/light-client-verifier/src/updates.rs b/crates/light-client-verifier/src/updates.rs index d9d91c0..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(()) } } @@ -119,3 +117,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;