diff --git a/aptos/docs/src/benchmark/configuration.md b/aptos/docs/src/benchmark/configuration.md index a749b873..95c44240 100644 --- a/aptos/docs/src/benchmark/configuration.md +++ b/aptos/docs/src/benchmark/configuration.md @@ -32,9 +32,10 @@ Here are the standard config variables that are worth setting for any benchmark: - `cargo +nightly-2024-05-31` - This ensures you are on a nightly toolchain. Nightly allows usage of AVX512 instructions which is crucial for performance. - This is the same version set on `rust-toolchain.toml`. It's pinned to a specific release (`v1.80.0-nightly`) to prevent - unexpected issues caused by newer Rust versions. + This ensures you are on a nightly toolchain. Nightly allows usage of AVX512 instructions which is crucial for + performance. This is the same version set on `rust-toolchain.toml`. It's pinned + to a specific release (`v1.80.0-nightly`) to prevent unexpected issues caused + by newer Rust versions. - `cargo bench --release <...>` diff --git a/ethereum/.cargo/config.toml b/ethereum/.cargo/config.toml index 0184a29b..a2991354 100644 --- a/ethereum/.cargo/config.toml +++ b/ethereum/.cargo/config.toml @@ -8,6 +8,7 @@ xclippy = [ "-Wclippy::all", "-Wclippy::cast_lossless", "-Wclippy::checked_conversions", + "-Wclippy::clone_on_copy", "-Wclippy::dbg_macro", "-Wclippy::disallowed_methods", "-Wclippy::derive_partial_eq_without_eq", @@ -35,11 +36,13 @@ xclippy = [ "-Wclippy::needless_for_each", "-Wclippy::needless_pass_by_value", "-Wclippy::option_option", + "-Wclippy::redundant_clone", "-Wclippy::same_functions_in_if_condition", "-Wclippy::single_match_else", "-Wclippy::trait_duplication_in_bounds", "-Wclippy::unnecessary_wraps", "-Wclippy::unnested_or_patterns", + "-Wclippy::unnecessary_to_owned", "-Wnonstandard_style", "-Wrust_2018_idioms", "-Wtrivial_numeric_casts", diff --git a/ethereum/Cargo.lock b/ethereum/Cargo.lock index 6624e9ee..63fef214 100644 --- a/ethereum/Cargo.lock +++ b/ethereum/Cargo.lock @@ -1439,6 +1439,7 @@ dependencies = [ "getset", "hex", "serde", + "serde_json", "sha2 0.9.9", "ssz_types", "thiserror", diff --git a/ethereum/core/Cargo.toml b/ethereum/core/Cargo.toml index 2b86c447..609a6af3 100644 --- a/ethereum/core/Cargo.toml +++ b/ethereum/core/Cargo.toml @@ -13,6 +13,7 @@ ethers-core = { workspace = true } hex = { workspace = true } getset = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, optional = true } sha2 = { workspace = true } thiserror = { workspace = true } tiny-keccak = { workspace = true, features = ["keccak"] } @@ -23,4 +24,10 @@ ethereum_ssz = { workspace = true } ethereum-types = { workspace = true } ssz_types = { workspace = true, features = ["arbitrary"] } tree_hash = { workspace = true } -tree_hash_derive = { workspace = true } \ No newline at end of file +tree_hash_derive = { workspace = true } + +[features] +default = [] +ethereum = [ + "dep:serde_json" +] \ No newline at end of file diff --git a/ethereum/core/src/lib.rs b/ethereum/core/src/lib.rs index e5ed329a..d27df0ff 100644 --- a/ethereum/core/src/lib.rs +++ b/ethereum/core/src/lib.rs @@ -9,6 +9,8 @@ //! ## Sub-modules //! //! - `crypto`: This sub-module contains the cryptographic utilities used by the Light Client. +//! - `merkle`: This sub-module contains the utilities to generate and verify Merkle proofs. +//! - `test_utils`: This sub-module contains utilities to help with testing the Light Client. //! - `types`: This sub-module contains the types and utilities necessary to prove sync committee changes //! and value inclusion in the state of the chain. //! @@ -16,4 +18,6 @@ pub mod crypto; pub mod merkle; +#[cfg(feature = "ethereum")] +pub mod test_utils; pub mod types; diff --git a/ethereum/core/src/merkle/update_proofs.rs b/ethereum/core/src/merkle/update_proofs.rs index 76376cea..a3883e70 100644 --- a/ethereum/core/src/merkle/update_proofs.rs +++ b/ethereum/core/src/merkle/update_proofs.rs @@ -6,7 +6,6 @@ use crate::merkle::error::MerkleError; use crate::merkle::utils::get_subtree_index; use crate::merkle::Merkleized; use crate::types::block::consensus::BeaconBlockHeader; -use crate::types::block::LightClientHeader; use crate::types::committee::{ SyncCommittee, SyncCommitteeBranch, CURRENT_SYNC_COMMITTEE_GENERALIZED_INDEX, NEXT_SYNC_COMMITTEE_GENERALIZED_INDEX, SYNC_COMMITTEE_BRANCH_NBR_SIBLINGS, @@ -20,7 +19,7 @@ use crate::types::{ /// /// # Arguments /// -/// * `attested_header` - The header of the block that the update is attesting to. +/// * `state_root` - The state root of the Beacon block that the proof is attesting to. /// * `finality_header` - The header of the block that the update is attesting to be finalized. /// * `finality_branch` - The branch of the Merkle tree that proves the finality of the block. /// @@ -28,12 +27,12 @@ use crate::types::{ /// /// A `bool` indicating whether the finality proof is valid. pub fn is_finality_proof_valid( - attested_header: &LightClientHeader, + state_root: &Bytes32, finality_header: &mut BeaconBlockHeader, finality_branch: &FinalizedRootBranch, ) -> Result { is_proof_valid( - attested_header, + state_root, finality_header, finality_branch, FINALIZED_CHECKPOINT_BRANCH_NBR_SIBLINGS, @@ -45,7 +44,7 @@ pub fn is_finality_proof_valid( /// /// # Arguments /// -/// * `attested_header` - The header of the block that the update is attesting to. +/// * `state_root` - The state root of the Beacon block that the proof is attesting to. /// * `sync_committee` - The next sync committee that the update is attesting to. /// * `sync_committee_branch` - The branch of the Merkle tree that proves the sync committee of the block. /// @@ -53,12 +52,12 @@ pub fn is_finality_proof_valid( /// /// A `bool` indicating whether the sync committee proof is valid. pub fn is_next_committee_proof_valid( - attested_header: &LightClientHeader, + state_root: &Bytes32, next_committee: &mut SyncCommittee, next_committee_branch: &SyncCommitteeBranch, ) -> Result { is_proof_valid( - attested_header, + state_root, next_committee, next_committee_branch, SYNC_COMMITTEE_BRANCH_NBR_SIBLINGS, @@ -70,7 +69,7 @@ pub fn is_next_committee_proof_valid( /// /// # Arguments /// -/// * `bootstrap_header` - The header of the block that the bootstrap is attesting to. +/// * `state_root` - The state root of the Beacon block that the proof is attesting to. /// * `current_committee` - The current sync committee that the bootstrap is attesting to. /// * `current_committee_branch` - The branch of the Merkle tree that proves the current committee of the block. /// @@ -78,12 +77,12 @@ pub fn is_next_committee_proof_valid( /// /// A `bool` indicating whether the current committee proof is valid. pub fn is_current_committee_proof_valid( - bootstrap_header: &LightClientHeader, + state_root: &Bytes32, current_committee: &mut SyncCommittee, current_committee_branch: &SyncCommitteeBranch, ) -> Result { is_proof_valid( - bootstrap_header, + state_root, current_committee, current_committee_branch, SYNC_COMMITTEE_BRANCH_NBR_SIBLINGS, @@ -95,7 +94,7 @@ pub fn is_current_committee_proof_valid( /// /// # Arguments /// -/// * `attested_header` - The header of the block that the update is attesting to. +/// * `state_root` - The state root of the Beacon block that the proof is attesting to. /// * `leaf_object` - The object that the proof is attesting to. /// * `branch` - The branch of the Merkle tree that proves the object. /// * `depth` - The depth of the Merkle tree. @@ -105,7 +104,7 @@ pub fn is_current_committee_proof_valid( /// /// A `bool` indicating whether the proof is valid. fn is_proof_valid( - attested_header: &LightClientHeader, + state_root: &Bytes32, leaf_object: &mut M, branch: &[Bytes32], depth: usize, @@ -143,7 +142,7 @@ fn is_proof_valid( .map_err(|err| MerkleError::Hash { source: err.into() })?; // 4. Instantiate expected root and siblings as `HashValue` - let state_root = HashValue::new(*attested_header.beacon().state_root()); + let state_root = HashValue::new(*state_root); let branch_hashes = branch .iter() .map(|bytes| HashValue::new(*bytes)) diff --git a/ethereum/light-client/src/test_utils.rs b/ethereum/core/src/test_utils.rs similarity index 87% rename from ethereum/light-client/src/test_utils.rs rename to ethereum/core/src/test_utils.rs index a7b20e97..0f7feb29 100644 --- a/ethereum/light-client/src/test_utils.rs +++ b/ethereum/core/src/test_utils.rs @@ -1,9 +1,10 @@ -use crate::types::storage::GetProofResponse; -use ethereum_lc_core::merkle::storage_proofs::EIP1186Proof; -use ethereum_lc_core::types::bootstrap::Bootstrap; -use ethereum_lc_core::types::store::LightClientStore; -use ethereum_lc_core::types::update::{FinalityUpdate, Update}; +use crate::merkle::storage_proofs::EIP1186Proof; +use crate::types::bootstrap::Bootstrap; +use crate::types::store::LightClientStore; +use crate::types::update::{FinalityUpdate, Update}; +use ethers_core::types::EIP1186ProofResponse; use getset::Getters; +use serde_json::Value; use std::fs; use std::path::PathBuf; @@ -105,7 +106,13 @@ pub fn generate_inclusion_test_assets() -> InclusionTestAssets { let test_bytes = fs::read(test_asset_path).unwrap(); - let ethers_eip1186_proof: GetProofResponse = serde_json::from_slice(&test_bytes).unwrap(); + let ethers_eip1186_proof: Value = serde_json::from_slice(&test_bytes).unwrap(); + + let call_res = ethers_eip1186_proof + .get("result") + .expect("Ethers EIP1186 proof result not found"); + let ethers_eip1186_proof: EIP1186ProofResponse = + serde_json::from_value(call_res.clone()).unwrap(); // Initialize the LightClientStore let trusted_block_root = hex::decode(INCLUSION_CHECKPOINT.strip_prefix("0x").unwrap()) @@ -120,6 +127,6 @@ pub fn generate_inclusion_test_assets() -> InclusionTestAssets { InclusionTestAssets { store, finality_update, - eip1186_proof: EIP1186Proof::try_from(ethers_eip1186_proof.result().clone()).unwrap(), + eip1186_proof: EIP1186Proof::try_from(ethers_eip1186_proof).unwrap(), } } diff --git a/ethereum/core/src/types/block/execution.rs b/ethereum/core/src/types/block/execution.rs index 5e690289..9e4902f2 100644 --- a/ethereum/core/src/types/block/execution.rs +++ b/ethereum/core/src/types/block/execution.rs @@ -350,7 +350,7 @@ pub(crate) mod test { // Hash for lighthouse implementation let test_execution_block_header = - ExecutionBlockHeaderTreeHash::try_from(execution_block_header.clone()).unwrap(); + ExecutionBlockHeaderTreeHash::try_from(execution_block_header).unwrap(); let execution_block_header_tree_hash = test_execution_block_header.tree_hash_root(); assert_eq!(hash_tree_root.hash(), execution_block_header_tree_hash.0); diff --git a/ethereum/core/src/types/block/mod.rs b/ethereum/core/src/types/block/mod.rs index 44789221..6fc15227 100644 --- a/ethereum/core/src/types/block/mod.rs +++ b/ethereum/core/src/types/block/mod.rs @@ -42,7 +42,7 @@ pub const LIGHT_CLIENT_HEADER_BASE_BYTES_LEN: usize = BEACON_BLOCK_HEADER_BYTES_ #[derive(Debug, Clone, Eq, PartialEq, Getters)] #[getset(get = "pub")] pub struct LightClientHeader { - beacon: BeaconBlockHeader, + pub(crate) beacon: BeaconBlockHeader, execution: ExecutionBlockHeader, execution_branch: ExecutionBranch, } diff --git a/ethereum/core/src/types/store.rs b/ethereum/core/src/types/store.rs index e8a56638..f8c20c09 100644 --- a/ethereum/core/src/types/store.rs +++ b/ethereum/core/src/types/store.rs @@ -20,7 +20,7 @@ use crate::types::bootstrap::Bootstrap; use crate::types::committee::{SyncCommittee, SyncCommitteeBranch, SYNC_COMMITTEE_BYTES_LEN}; use crate::types::error::{ConsensusError, StoreError, TypesError}; use crate::types::signing_data::SigningData; -use crate::types::update::Update; +use crate::types::update::{CompactUpdate, Update}; use crate::types::utils::{ calc_sync_period, extract_u32, extract_u64, DOMAIN_BEACON_DENEB, OFFSET_BYTE_LENGTH, U64_LEN, }; @@ -52,6 +52,34 @@ pub struct LightClientStore { } impl LightClientStore { + /// Consumes the `LightClientStore` and returns the `current_sync_committee`. + /// + /// This method moves the `current_sync_committee` field out + /// of the `LightClientStore` struct, consuming the struct in the process. + /// As a result, the `LightClientStore` cannot be used after this method is called. + /// + /// # Returns + /// + /// The current sync committee. + pub fn into_current_sync_committee(self) -> SyncCommittee { + self.current_sync_committee + } + + /// Consumes the `LightClientStore` and returns the `next_sync_committee`, + /// if it exists. + /// + /// This method moves the `next_sync_committee` field out of the + /// `LightClientStore` struct, consuming the struct in the process. + /// As a result, the `LightClientStore` cannot be used after this + /// method is called. + /// + /// # Returns + /// + /// - `Option`: An `Option` containing the next synchronization committee if it exists, or `None` if it does not. + pub fn into_next_sync_committee(self) -> Option { + self.next_sync_committee + } + /// Initializes the `LightClientStore` with the given `Bootstrap` data. /// /// # Arguments @@ -81,7 +109,7 @@ impl LightClientStore { // Confirm that the given sync committee was committed in the block let is_valid = is_current_committee_proof_valid( - bootstrap.header(), + bootstrap.header().beacon().state_root(), &mut bootstrap.current_sync_committee().clone(), bootstrap.current_sync_committee_branch(), ) @@ -228,7 +256,7 @@ impl LightClientStore { // Ensure that the received finality proof is valid let is_valid = is_finality_proof_valid( - update.attested_header(), + update.attested_header().beacon().state_root(), &mut update.finalized_header().beacon().clone(), update.finality_branch(), ) @@ -245,7 +273,7 @@ impl LightClientStore { } } else { let is_valid = is_next_committee_proof_valid( - update.attested_header(), + update.attested_header().beacon().state_root(), &mut update.next_sync_committee().clone(), update.next_sync_committee_branch(), ) @@ -477,68 +505,167 @@ impl LightClientStore { } } -#[cfg(test)] -mod test { - use crate::merkle::Merkleized; - use crate::types::bootstrap::Bootstrap; - use crate::types::store::LightClientStore; - use crate::types::update::Update; - use std::env::current_dir; - use std::fs; - - struct TestAssets { - store: LightClientStore, - update: Update, - update_new_period: Update, +/// Data structure used to represent a compact store. This is a reduced +/// version of the [`LightClientStore`] that is used to store the minimum +/// amount of data necessary to verify a [`CompactUpdate`]. +#[derive(Debug, Clone, Eq, PartialEq, Getters)] +#[getset(get = "pub")] +pub struct CompactStore { + finalized_beacon_header_slot: u64, + sync_committee: SyncCommittee, +} + +impl CompactStore { + /// Initializes the `CompactStore` with the given finalized beacon + /// header slot and `SyncCommittee`. + /// + /// # Arguments + /// + /// * `finalized_beacon_header_slot` - The slot of the finalized beacon header. + /// * `sync_committee` - The `SyncCommittee` to initialize the store. + /// + /// # Returns + /// + /// The initialized `CompactStore`. + pub const fn new(finalized_beacon_header_slot: u64, sync_committee: SyncCommittee) -> Self { + Self { + finalized_beacon_header_slot, + sync_committee, + } } - fn generate_test_assets() -> TestAssets { - // Instantiate bootstrap data - let test_asset_path = current_dir() - .unwrap() - .join("../test-assets/committee-change/LightClientBootstrapDeneb.ssz"); + /// Serializes the `CompactStore` into SSZ bytes. + /// + /// # Returns + /// + /// The SSZ bytes of the `CompactStore`. + pub fn to_ssz_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + + // Serialize the snapshot period + bytes.extend_from_slice(&self.finalized_beacon_header_slot.to_le_bytes()); + bytes.extend_from_slice(&self.sync_committee.to_ssz_bytes()); - let test_bytes = fs::read(test_asset_path).unwrap(); + bytes + } - let bootstrap = Bootstrap::from_ssz_bytes(&test_bytes).unwrap(); + /// Deserializes the `CompactStore` from SSZ bytes. + /// + /// # Arguments + /// + /// * `bytes` - The SSZ bytes to deserialize. + /// + /// # Returns + /// + /// A `Result` containing the deserialized `CompactStore` or a `TypesError` if the bytes are + /// invalid. + pub fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != U64_LEN + SYNC_COMMITTEE_BYTES_LEN { + return Err(TypesError::UnderLength { + minimum: U64_LEN + SYNC_COMMITTEE_BYTES_LEN, + actual: bytes.len(), + structure: "CompactStore".into(), + }); + } - // Instantiate Update data - let test_asset_path = current_dir() - .unwrap() - .join("../test-assets/committee-change/LightClientUpdateDeneb.ssz"); + // Deserialize the snapshot period + let finalized_beacon_header_slot = u64::from_le_bytes(bytes[..U64_LEN].try_into().unwrap()); - let test_bytes = fs::read(test_asset_path).unwrap(); + // Deserialize the sync committee + let sync_committee = SyncCommittee::from_ssz_bytes(&bytes[U64_LEN..])?; - let update = Update::from_ssz_bytes(&test_bytes).unwrap(); + Ok(Self { + finalized_beacon_header_slot, + sync_committee, + }) + } - // Instantiate new period Update data - let test_asset_path = current_dir() - .unwrap() - .join("../test-assets/committee-change/LightClientUpdateNewPeriodDeneb.ssz"); + /// Validates the received `CompactUpdate` against the current + /// state of the `CompactStore`. + /// + /// # Arguments + /// + /// * `update` - The `CompactUpdate` to validate. + /// + /// # Returns + /// + /// A `Result` containing `()` if the update is valid, or a `ConsensusError` + /// if the update is invalid. + pub fn validate_compact_update(&self, update: &CompactUpdate) -> Result<(), ConsensusError> { + // Ensure we at least have 1 signer + if update + .sync_aggregate() + .sync_committee_bits() + .iter() + .map(|&bit| u64::from(bit)) + .sum::() + < 1 + { + return Err(ConsensusError::InsufficientSigners); + } - let test_bytes = fs::read(test_asset_path).unwrap(); + // Assert that the received data make sense chronologically + let valid_time = update.signature_slot() > update.attested_beacon_header().slot() + && update.attested_beacon_header().slot() >= update.finalized_beacon_header().slot(); - let update_new_period = Update::from_ssz_bytes(&test_bytes).unwrap(); + if !valid_time { + return Err(ConsensusError::InvalidTimestamp); + } - // Initialize the LightClientStore - let checkpoint = "0xefb4338d596b9d335b2da176dc85ee97469fc80c7e2d35b9b9c1558b4602077a"; - let trusted_block_root = hex::decode(checkpoint.strip_prefix("0x").unwrap()) - .unwrap() - .try_into() - .unwrap(); + let snapshot_period = calc_sync_period(self.finalized_beacon_header_slot()); + let update_sig_period = calc_sync_period(update.signature_slot()); + if snapshot_period != update_sig_period { + return Err(ConsensusError::InvalidPeriod); + } - let store = LightClientStore::initialize(trusted_block_root, &bootstrap).unwrap(); + // Ensure that the received finality proof is valid + let is_valid = is_finality_proof_valid( + update.attested_beacon_header().state_root(), + &mut update.finalized_beacon_header().clone(), + update.finality_branch(), + ) + .map_err(|err| ConsensusError::MerkleError { source: err.into() })?; - TestAssets { - store, - update, - update_new_period, + if !is_valid { + return Err(ConsensusError::InvalidFinalityProof); } + + let pks = self + .sync_committee + .get_participant_pubkeys(update.sync_aggregate().sync_committee_bits()); + + let header_root = update + .attested_beacon_header() + .hash_tree_root() + .map_err(|err| ConsensusError::MerkleError { source: err.into() })?; + + let signing_data = SigningData::new(header_root.hash(), DOMAIN_BEACON_DENEB); + + let signing_root = signing_data + .hash_tree_root() + .map_err(|err| ConsensusError::MerkleError { source: err.into() })?; + + let aggregated_pubkey = PublicKey::aggregate(&pks) + .map_err(|err| ConsensusError::SignatureError { source: err.into() })?; + + update + .sync_aggregate() + .sync_committee_signature() + .verify(signing_root.as_ref(), &aggregated_pubkey) + .map_err(|err| ConsensusError::SignatureError { source: err.into() }) } +} + +#[cfg(feature = "ethereum")] +#[cfg(test)] +mod test { + use crate::merkle::Merkleized; + use crate::test_utils::generate_committee_change_test_assets; + use crate::types::store::{CompactStore, LightClientStore}; #[test] fn test_simple_validate_and_apply_update() { - let mut test_assets = generate_test_assets(); + let mut test_assets = generate_committee_change_test_assets(); test_assets .store @@ -568,7 +695,7 @@ mod test { #[test] fn test_process_update() { - let mut test_assets = generate_test_assets(); + let mut test_assets = generate_committee_change_test_assets(); // Note: The data is not passed through process_light_client_update as the update is never applied because quorum is not met on the static data @@ -639,8 +766,8 @@ mod test { } #[test] - fn test_ssz_serde() { - let test_assets = generate_test_assets(); + fn test_ssz_serde_light_client_store() { + let test_assets = generate_committee_change_test_assets(); let bytes = test_assets.store.to_ssz_bytes().unwrap(); @@ -648,4 +775,20 @@ mod test { assert_eq!(test_assets.store, deserialized_store.unwrap()); } + + #[test] + fn test_ssz_serde_compact_store() { + let test_assets = generate_committee_change_test_assets(); + + let compact_store = CompactStore::new( + *test_assets.store.finalized_header().beacon().slot(), + test_assets.store.current_sync_committee().clone(), + ); + + let serialized_store = compact_store.to_ssz_bytes(); + + let deserialized_store = CompactStore::from_ssz_bytes(&serialized_store).unwrap(); + + assert_eq!(compact_store, deserialized_store); + } } diff --git a/ethereum/core/src/types/update.rs b/ethereum/core/src/types/update.rs index df9ab372..60eb1f22 100644 --- a/ethereum/core/src/types/update.rs +++ b/ethereum/core/src/types/update.rs @@ -9,6 +9,7 @@ use crate::crypto::sig::{SyncAggregate, SYNC_AGGREGATE_BYTES_LEN}; use crate::deserialization_error; +use crate::types::block::consensus::BeaconBlockHeader; use crate::types::block::{LightClientHeader, LIGHT_CLIENT_HEADER_BASE_BYTES_LEN}; use crate::types::committee::{ SyncCommittee, SyncCommitteeBranch, SYNC_COMMITTEE_BRANCH_NBR_SIBLINGS, @@ -16,7 +17,9 @@ use crate::types::committee::{ }; use crate::types::error::TypesError; use crate::types::utils::{extract_u32, extract_u64, OFFSET_BYTE_LENGTH, U64_LEN}; -use crate::types::{FinalizedRootBranch, BYTES_32_LEN, FINALIZED_CHECKPOINT_BRANCH_NBR_SIBLINGS}; +use crate::types::{ + Bytes32, FinalizedRootBranch, BYTES_32_LEN, FINALIZED_CHECKPOINT_BRANCH_NBR_SIBLINGS, +}; use getset::Getters; /// Base length of a `Update` struct in bytes. @@ -33,10 +36,10 @@ pub const UPDATE_BASE_BYTES_LEN: usize = LIGHT_CLIENT_HEADER_BASE_BYTES_LEN * 2 #[derive(Debug, Clone, Eq, PartialEq, Getters)] #[getset(get = "pub")] pub struct Update { - attested_header: LightClientHeader, + pub(crate) attested_header: LightClientHeader, next_sync_committee: SyncCommittee, next_sync_committee_branch: SyncCommitteeBranch, - finalized_header: LightClientHeader, + pub(crate) finalized_header: LightClientHeader, finality_branch: FinalizedRootBranch, sync_aggregate: SyncAggregate, signature_slot: u64, @@ -368,6 +371,199 @@ impl FinalityUpdate { } } +/// Base length of a `CompactUpdate` struct in SSZ bytes. +pub const COMPACT_ATTESTED_BEACON_OFFSET: usize = OFFSET_BYTE_LENGTH * 2 + + BYTES_32_LEN + + FINALIZED_CHECKPOINT_BRANCH_NBR_SIBLINGS * BYTES_32_LEN + + SYNC_AGGREGATE_BYTES_LEN + + U64_LEN; + +/// A compact representation of a `Update` struct. +#[derive(Debug, Clone, Eq, PartialEq, Getters)] +#[getset(get = "pub")] +pub struct CompactUpdate { + attested_beacon_header: BeaconBlockHeader, + finalized_beacon_header: BeaconBlockHeader, + finalized_execution_state_root: Bytes32, + finality_branch: FinalizedRootBranch, + sync_aggregate: SyncAggregate, + signature_slot: u64, +} + +impl From for CompactUpdate { + fn from(finality_update: FinalityUpdate) -> Self { + Self { + attested_beacon_header: finality_update.attested_header.beacon().clone(), + finalized_beacon_header: finality_update.finalized_header.beacon().clone(), + finalized_execution_state_root: *finality_update + .finalized_header + .execution() + .state_root(), + finality_branch: finality_update.finality_branch, + sync_aggregate: finality_update.sync_aggregate, + signature_slot: finality_update.signature_slot, + } + } +} + +impl From for CompactUpdate { + fn from(update: Update) -> Self { + Self { + finalized_execution_state_root: *update.finalized_header.execution().state_root(), + attested_beacon_header: update.attested_header.beacon, + finalized_beacon_header: update.finalized_header.beacon, + finality_branch: update.finality_branch, + sync_aggregate: update.sync_aggregate, + signature_slot: update.signature_slot, + } + } +} + +impl CompactUpdate { + /// Serialize the `CompactUpdate` struct to SSZ bytes. + /// + /// # Returns + /// + /// A `Vec` containing the SSZ serialized `CompactUpdate` struct. + pub fn to_ssz_bytes(&self) -> Result, TypesError> { + let mut bytes = vec![]; + + // Serialize attested beacon header + bytes.extend_from_slice(&(COMPACT_ATTESTED_BEACON_OFFSET as u32).to_le_bytes()); + let attested_header_bytes = self.attested_beacon_header.to_ssz_bytes(); + + // Serialize finalized beacon block header + let finalized_beacon_block_header_offset = + attested_header_bytes.len() + COMPACT_ATTESTED_BEACON_OFFSET; + bytes.extend_from_slice(&(finalized_beacon_block_header_offset as u32).to_le_bytes()); + let finalized_beacon_block_header_bytes = self.finalized_beacon_header.to_ssz_bytes(); + + // Serialize finalized execution state root + bytes.extend_from_slice(&self.finalized_execution_state_root); + + // Serialize finality branch + for root in &self.finality_branch { + bytes.extend(root); + } + + // Serialize sync aggregate + bytes.extend(&self.sync_aggregate.to_ssz_bytes()?); + + // Serialize signature slot + bytes.extend_from_slice(&self.signature_slot.to_le_bytes()); + + // Append attested beacon header bytes + bytes.extend(&attested_header_bytes); + + // Append finalized beacon block header bytes + bytes.extend(&finalized_beacon_block_header_bytes); + + Ok(bytes) + } + + /// Deserialize a `CompactUpdate` struct from SSZ bytes. + /// + /// # Arguments + /// + /// * `bytes` - The SSZ encoded bytes. + /// + /// # Returns + /// + /// A `Result` containing the deserialized `CompactUpdate` struct or a `TypesError`. + pub fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() < COMPACT_ATTESTED_BEACON_OFFSET { + return Err(TypesError::UnderLength { + minimum: COMPACT_ATTESTED_BEACON_OFFSET, + actual: bytes.len(), + structure: "CompactUpdate".into(), + }); + } + + let cursor = 0; + + // Deserialize attested beacon header offset + let (cursor, offset_attested_beacon_header) = extract_u32("CompactUpdate", bytes, cursor)?; + + // Deserialize finalized beacon block header offset + let (cursor, offset_finalized_beacon_block_header) = + extract_u32("CompactUpdate", bytes, cursor)?; + + // Deserialize finalized execution state root + let finalized_execution_state_root = bytes[cursor..cursor + BYTES_32_LEN] + .try_into() + .map_err(|_| { + deserialization_error!("CompactUpdate", "Failed to convert bytes into Bytes32") + })?; + + // Deserialize finality branch + let cursor = cursor + BYTES_32_LEN; + let finality_branch = (0..FINALIZED_CHECKPOINT_BRANCH_NBR_SIBLINGS) + .map(|i| { + let start = cursor + i * BYTES_32_LEN; + let end = start + BYTES_32_LEN; + let returned_bytes = &bytes[start..end]; + returned_bytes.try_into().map_err(|_| { + deserialization_error!( + "CompactUpdate", + "Failed to convert bytes into FinalizedRootBranch" + ) + }) + }) + .collect::, _>>()? + .try_into() + .map_err(|_| { + deserialization_error!( + "CompactUpdate", + "Failed to convert bytes into FinalizedRootBranch" + ) + })?; + + // Deserialize sync aggregate + let cursor = cursor + FINALIZED_CHECKPOINT_BRANCH_NBR_SIBLINGS * BYTES_32_LEN; + let sync_aggregate = + SyncAggregate::from_ssz_bytes(&bytes[cursor..cursor + SYNC_AGGREGATE_BYTES_LEN])?; + + // Deserialize signature slot + let cursor = cursor + SYNC_AGGREGATE_BYTES_LEN; + let (cursor, signature_slot) = extract_u64("CompactUpdate", bytes, cursor)?; + + // Deserialize attested beacon header + if cursor != offset_attested_beacon_header as usize { + return Err(deserialization_error!( + "CompactUpdate", + "Invalid offset for attested beacon header" + )); + } + + let attested_beacon_header = BeaconBlockHeader::from_ssz_bytes( + &bytes[cursor + ..cursor + offset_finalized_beacon_block_header as usize + - offset_attested_beacon_header as usize], + )?; + + // Deserialize finalized beacon block header + let cursor = cursor + offset_finalized_beacon_block_header as usize + - offset_attested_beacon_header as usize; + if cursor != offset_finalized_beacon_block_header as usize { + return Err(deserialization_error!( + "CompactUpdate", + "Invalid offset for finalized beacon block header" + )); + } + + let finalized_beacon_header = BeaconBlockHeader::from_ssz_bytes(&bytes[cursor..])?; + + Ok(Self { + attested_beacon_header, + finalized_beacon_header, + finalized_execution_state_root, + finality_branch, + sync_aggregate, + signature_slot, + }) + } +} + #[cfg(test)] mod test { use super::*; @@ -401,7 +597,7 @@ mod test { let update = Update::from_ssz_bytes(&test_bytes).unwrap(); let valid = is_next_committee_proof_valid( - update.attested_header(), + update.attested_header().beacon().state_root(), &mut update.next_sync_committee().clone(), update.next_sync_committee_branch(), ) @@ -410,7 +606,7 @@ mod test { assert!(valid); let valid = is_finality_proof_valid( - update.attested_header(), + update.attested_header().beacon().state_root(), &mut update.finalized_header().beacon().clone(), update.finality_branch(), ) @@ -433,4 +629,23 @@ mod test { assert_eq!(ssz_bytes, test_bytes); } + + #[test] + fn test_ssz_serde_compact_update() { + let test_asset_path = current_dir() + .unwrap() + .join("../test-assets/inclusion/LightClientFinalityUpdateDeneb.ssz"); + + let test_bytes = fs::read(test_asset_path).unwrap(); + + let finality_update = FinalityUpdate::from_ssz_bytes(&test_bytes).unwrap(); + + let compact_update = CompactUpdate::from(finality_update); + + let ssz_bytes = compact_update.to_ssz_bytes().unwrap(); + + let deserialized_compact_update = CompactUpdate::from_ssz_bytes(&ssz_bytes).unwrap(); + + assert_eq!(compact_update, deserialized_compact_update); + } } diff --git a/ethereum/docs/src/benchmark/proof.md b/ethereum/docs/src/benchmark/proof.md index 49362d56..77fc44e1 100644 --- a/ethereum/docs/src/benchmark/proof.md +++ b/ethereum/docs/src/benchmark/proof.md @@ -134,7 +134,7 @@ To run the test efficiently, first install `nextest` following [its documentatio Ensure that you also have the previously described environment variables set, then run the following command: ```shell -SHARD_BATCH_SIZE=0 cargo +nightly-2024-05-31 nextest run --verbose --release --profile ci --package ethereum-lc --no-capture +SHARD_BATCH_SIZE=0 cargo +nightly-2024-05-31 nextest run --verbose --release --profile ci --package ethereum-lc --no-capture --all-features ``` > **Note** diff --git a/ethereum/ethereum-programs/artifacts/committee-change-program b/ethereum/ethereum-programs/artifacts/committee-change-program index 8f720269..9088542a 100755 Binary files a/ethereum/ethereum-programs/artifacts/committee-change-program and b/ethereum/ethereum-programs/artifacts/committee-change-program differ diff --git a/ethereum/ethereum-programs/artifacts/inclusion-program b/ethereum/ethereum-programs/artifacts/inclusion-program index 39ded448..553dae71 100755 Binary files a/ethereum/ethereum-programs/artifacts/inclusion-program and b/ethereum/ethereum-programs/artifacts/inclusion-program differ diff --git a/ethereum/light-client/Cargo.toml b/ethereum/light-client/Cargo.toml index d393afc0..6eb9691a 100644 --- a/ethereum/light-client/Cargo.toml +++ b/ethereum/light-client/Cargo.toml @@ -45,4 +45,4 @@ name = "inclusion" harness = false [features] -ethereum = [] +ethereum = ["ethereum-lc-core/ethereum"] diff --git a/ethereum/light-client/src/lib.rs b/ethereum/light-client/src/lib.rs index c4dc7c9f..dccfad12 100644 --- a/ethereum/light-client/src/lib.rs +++ b/ethereum/light-client/src/lib.rs @@ -25,6 +25,7 @@ //! - [`client`] : The client that can be used to coordinate data fetching from the remote services. //! - [`proofs`]: The utilities to generate and verify proofs for the light client. //! - [`types`]: Types and utilities to leverage data from the remote services. +//! - [`utils`]: Utilities to help with the light client. //! //! For more detailed information, users should refer to the specific documentation for each //! sub-module. @@ -32,6 +33,6 @@ pub mod client; pub mod proofs; #[cfg(feature = "ethereum")] -pub mod test_utils; +pub use ethereum_lc_core::test_utils; pub mod types; pub mod utils; diff --git a/ethereum/light-client/src/proofs/inclusion.rs b/ethereum/light-client/src/proofs/inclusion.rs index 27371e9a..3d7256dc 100644 --- a/ethereum/light-client/src/proofs/inclusion.rs +++ b/ethereum/light-client/src/proofs/inclusion.rs @@ -13,9 +13,9 @@ use ethereum_lc_core::crypto::hash::{HashValue, HASH_LENGTH}; use ethereum_lc_core::deserialization_error; use ethereum_lc_core::merkle::storage_proofs::EIP1186Proof; use ethereum_lc_core::types::error::TypesError; -use ethereum_lc_core::types::store::LightClientStore; -use ethereum_lc_core::types::update::Update; -use ethereum_lc_core::types::utils::{extract_u32, OFFSET_BYTE_LENGTH}; +use ethereum_lc_core::types::store::{CompactStore, LightClientStore}; +use ethereum_lc_core::types::update::{CompactUpdate, Update}; +use ethereum_lc_core::types::utils::{calc_sync_period, extract_u32, OFFSET_BYTE_LENGTH}; use ethereum_lc_core::types::{Address, ADDRESS_BYTES_LEN}; use ethereum_programs::INCLUSION_PROGRAM; use getset::{CopyGetters, Getters}; @@ -213,15 +213,27 @@ impl Prover for StorageInclusionProver { fn generate_sphinx_stdin(&self, inputs: Self::StdIn) -> Result { let mut stdin = SphinxStdin::new(); - stdin.write( - &inputs + + let update_sig_period = calc_sync_period(inputs.update.signature_slot()); + let store_period = calc_sync_period(inputs.store.finalized_header().beacon().slot()); + + let finalized_beacon_slot = *inputs.store.finalized_header().beacon().slot(); + let correct_sync_committee = if update_sig_period == store_period { + inputs.store.into_current_sync_committee() + } else { + inputs .store - .to_ssz_bytes() - .map_err(|err| ProverError::SphinxInput { source: err.into() })?, + .into_next_sync_committee() + .ok_or_else(|| ProverError::SphinxInput { + source: "Expected next sync committee".into(), + })? + }; + + stdin.write( + &CompactStore::new(finalized_beacon_slot, correct_sync_committee).to_ssz_bytes(), ); stdin.write( - &inputs - .update + &CompactUpdate::from(inputs.update) .to_ssz_bytes() .map_err(|err| ProverError::SphinxInput { source: err.into() })?, ); diff --git a/ethereum/programs/inclusion/src/main.rs b/ethereum/programs/inclusion/src/main.rs index dfdab366..9fa60914 100644 --- a/ethereum/programs/inclusion/src/main.rs +++ b/ethereum/programs/inclusion/src/main.rs @@ -5,9 +5,8 @@ use ethereum_lc_core::crypto::hash::keccak256_hash; use ethereum_lc_core::merkle::storage_proofs::EIP1186Proof; -use ethereum_lc_core::types::store::LightClientStore; -use ethereum_lc_core::types::update::Update; -use ethereum_lc_core::types::utils::calc_sync_period; +use ethereum_lc_core::types::store::CompactStore; +use ethereum_lc_core::types::update::CompactUpdate; sphinx_zkvm::entrypoint!(main); @@ -15,8 +14,8 @@ pub fn main() { sphinx_zkvm::precompiles::unconstrained! { println!("cycle-tracker-start: read_inputs"); } - let store_bytes = sphinx_zkvm::io::read::>(); - let update_bytes = sphinx_zkvm::io::read::>(); + let compact_store_bytes = sphinx_zkvm::io::read::>(); + let compact_update_bytes = sphinx_zkvm::io::read::>(); let eip1186_proof_bytes = sphinx_zkvm::io::read::>(); sphinx_zkvm::precompiles::unconstrained! { println!("cycle-tracker-end: read_inputs"); @@ -25,10 +24,10 @@ pub fn main() { sphinx_zkvm::precompiles::unconstrained! { println!("cycle-tracker-start: deserialize_inputs"); } - let store = LightClientStore::from_ssz_bytes(&store_bytes) - .expect("LightClientStore::from_ssz_bytes: could not create store"); - let update = Update::from_ssz_bytes(&update_bytes) - .expect("Update::from_ssz_bytes: could not create update"); + let compact_store = CompactStore::from_ssz_bytes(&compact_store_bytes) + .expect("CompactStore::from_ssz_bytes: could not create store"); + let compact_update = CompactUpdate::from_ssz_bytes(&compact_update_bytes) + .expect("CompactUpdate::from_ssz_bytes: could not create update"); let eip1186_proof = EIP1186Proof::from_ssz_bytes(&eip1186_proof_bytes) .expect("EIP1186Proof::from_ssz_bytes: could not create proof"); sphinx_zkvm::precompiles::unconstrained! { @@ -39,8 +38,8 @@ pub fn main() { sphinx_zkvm::precompiles::unconstrained! { println!("cycle-tracker-start: validate_update"); } - store - .validate_light_client_update(&update) + compact_store + .validate_compact_update(&compact_update) .expect("validate_light_client_update: could not validate update"); sphinx_zkvm::precompiles::unconstrained! { println!("cycle-tracker-end: validate_update"); @@ -51,7 +50,7 @@ pub fn main() { println!("cycle-tracker-start: verify_proof"); } eip1186_proof - .verify(update.finalized_header().execution().state_root()) + .verify(compact_update.finalized_execution_state_root()) .expect("verify: could not verify proof"); sphinx_zkvm::precompiles::unconstrained! { println!("cycle-tracker-end: verify_proof"); @@ -61,16 +60,11 @@ pub fn main() { sphinx_zkvm::precompiles::unconstrained! { println!("cycle-tracker-start: output"); } - let update_sig_period = calc_sync_period(update.signature_slot()); - let store_period = calc_sync_period(store.finalized_header().beacon().slot()); - let sync_committee = if update_sig_period == store_period { - store.current_sync_committee - } else { - store.next_sync_committee().unwrap() - }; - let sync_committee_hash = keccak256_hash(&sync_committee.to_ssz_bytes()) - .expect("LightClientStore::current_sync_committee: could not hash committee after inclusion proving"); - sphinx_zkvm::io::commit(update.finalized_header().beacon().slot()); + let sync_committee_hash = keccak256_hash(&compact_store.sync_committee().to_ssz_bytes()) + .expect( + "CompactStore::current_sync_committee: could not hash committee after inclusion proving", + ); + sphinx_zkvm::io::commit(compact_update.finalized_beacon_header().slot()); sphinx_zkvm::io::commit(sync_committee_hash.as_ref()); // Account key sphinx_zkvm::io::commit(&eip1186_proof.address);