From a3dd5ab6e7e9f45ec1253c8d96ebde8c641f5cf5 Mon Sep 17 00:00:00 2001 From: Sulpiride Date: Tue, 30 Jul 2024 23:30:18 +0500 Subject: [PATCH 1/5] wip: eip4881 structs --- Cargo.lock | 1 + deposit_tree/Cargo.toml | 1 + deposit_tree/src/eip_4881/mod.rs | 2 + deposit_tree/src/eip_4881/snapshot.rs | 63 +++++ deposit_tree/src/eip_4881/tree.rs | 9 + deposit_tree/src/lib.rs | 376 +------------------------- deposit_tree/src/tree.rs | 373 +++++++++++++++++++++++++ 7 files changed, 453 insertions(+), 372 deletions(-) create mode 100644 deposit_tree/src/eip_4881/mod.rs create mode 100644 deposit_tree/src/eip_4881/snapshot.rs create mode 100644 deposit_tree/src/eip_4881/tree.rs create mode 100644 deposit_tree/src/tree.rs diff --git a/Cargo.lock b/Cargo.lock index 0e288106..46466eea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1753,6 +1753,7 @@ dependencies = [ "anyhow", "factory", "features", + "hashing", "interop", "itertools 0.13.0", "spec_test_utils", diff --git a/deposit_tree/Cargo.toml b/deposit_tree/Cargo.toml index 90899b85..d16c2f47 100644 --- a/deposit_tree/Cargo.toml +++ b/deposit_tree/Cargo.toml @@ -14,6 +14,7 @@ ssz = { workspace = true } thiserror = { workspace = true } typenum = { workspace = true } types = { workspace = true } +hashing = { workspace = true } [dev-dependencies] factory = { workspace = true } diff --git a/deposit_tree/src/eip_4881/mod.rs b/deposit_tree/src/eip_4881/mod.rs new file mode 100644 index 00000000..27140b9b --- /dev/null +++ b/deposit_tree/src/eip_4881/mod.rs @@ -0,0 +1,2 @@ +pub mod snapshot; +pub mod tree; diff --git a/deposit_tree/src/eip_4881/snapshot.rs b/deposit_tree/src/eip_4881/snapshot.rs new file mode 100644 index 00000000..8221556a --- /dev/null +++ b/deposit_tree/src/eip_4881/snapshot.rs @@ -0,0 +1,63 @@ +use ssz::{hashing::ZERO_HASHES, PersistentList, Ssz}; +use typenum::Unsigned; +use types::phase0::{ + consts::DepositContractTreeDepth, + primitives::{DepositIndex, ExecutionBlockNumber, H256}, +}; +use hashing::hash_256_256; + +pub type FinalizedDeposit = PersistentList; + +// This is an implementation of a deposit tree snapshot described in EIP-4881 +// ref: https://eips.ethereum.org/EIPS/eip-4881#reference-implementation +#[derive(Clone, Default, Ssz)] +#[ssz(derive_hash = false)] +pub struct DepositTreeSnapshot { + // proof of the latest finalized deposit + finalized: FinalizedDeposit, + // same as Eth1Data + deposit_root: H256, + deposit_count: DepositIndex, + execution_block_hash: H256, + execution_block_height: ExecutionBlockNumber, +} + +impl DepositTreeSnapshot { + #[must_use] + pub fn calculate_root(&self) -> H256 { + let mut size = self.deposit_count; + let mut index = self.finalized.len_u64(); + let mut root = ZERO_HASHES[0]; + ZERO_HASHES + .iter() + .take(DepositContractTreeDepth::USIZE) + .for_each(|zero_hash| { + if size & 1 == 1 { + index -= 1; + // this is safe because index is never bigger than finalized.len() + root = hash_256_256(*self.finalized.get(index).unwrap(), root); + } else { + root = hash_256_256(root, *zero_hash); + } + size >>= 1; + }); + hash_256_256(root, H256::from_slice(&self.deposit_count.to_le_bytes())) + } + + #[must_use] + pub fn from_tree_parts( + finalized: FinalizedDeposit, + deposit_count: DepositIndex, + execution_block: (H256, ExecutionBlockNumber) + ) -> Self { + let mut snapshot = DepositTreeSnapshot { + finalized, + deposit_root: ZERO_HASHES[0], + deposit_count, + execution_block_hash: execution_block.0, + execution_block_height: execution_block.1 + }; + snapshot.deposit_root = snapshot.calculate_root(); + snapshot + } +} \ No newline at end of file diff --git a/deposit_tree/src/eip_4881/tree.rs b/deposit_tree/src/eip_4881/tree.rs new file mode 100644 index 00000000..be282397 --- /dev/null +++ b/deposit_tree/src/eip_4881/tree.rs @@ -0,0 +1,9 @@ +use ssz::MerkleTree; +use types::phase0::{consts::DepositContractTreeDepth, containers::Eth1Data, primitives::ExecutionBlockNumber}; + +use crate::DepositTreeSnapshot; + +pub struct DepositTree { + pub merkle_tree: MerkleTree, + pub mix_in_length: u64, +} \ No newline at end of file diff --git a/deposit_tree/src/lib.rs b/deposit_tree/src/lib.rs index 914f7b0f..7a8d022f 100644 --- a/deposit_tree/src/lib.rs +++ b/deposit_tree/src/lib.rs @@ -1,373 +1,5 @@ -use core::ops::Range; +pub mod tree; +pub mod eip_4881; -use anyhow::{ensure, Result}; -use itertools::Itertools as _; -use ssz::{MerkleTree, Ssz, SszHash as _}; -use thiserror::Error; -use typenum::Unsigned as _; -use types::phase0::{ - consts::DepositContractTreeDepth, - containers::{Deposit, DepositData}, - primitives::{DepositIndex, ExecutionBlockNumber, H256}, -}; - -const MAX_DEPOSITS: DepositIndex = 1 << DepositContractTreeDepth::USIZE; - -// We do not store the whole deposit tree, only hashes that are needed to construct proofs. -// These implementations appear to use the same algorithm: -// - -// - -// - -// -// See the [reference implementation in EIP-4881] for another approach. -// -// [reference implementation in EIP-4881]: https://eips.ethereum.org/EIPS/eip-4881#reference-implementation -#[derive(Clone, Copy, Default, Ssz)] -#[ssz(derive_hash = false)] -pub struct DepositTree { - pub merkle_tree: MerkleTree, - pub deposit_count: DepositIndex, - // Latest Eth1 block from which deposits were added to deposit tree - pub last_added_block_number: ExecutionBlockNumber, -} - -impl DepositTree { - pub fn push(&mut self, index: DepositIndex, data: DepositData) -> Result<()> { - features::log!( - DebugEth1, - "DepositTree::push (self.deposit_count: {}, index: {index}, data: {data:?})", - self.deposit_count, - ); - - let index = self.validate_index(index)?; - let chunk = data.hash_tree_root(); - - self.merkle_tree.push(index, chunk); - self.deposit_count += 1; - - Ok(()) - } - - pub fn push_and_compute_root( - &mut self, - index: DepositIndex, - data: DepositData, - ) -> Result { - features::log!( - DebugEth1, - "DepositTree::push_and_compute_root \ - (self.deposit_count: {}, index: {index}, data: {data:?})", - self.deposit_count, - ); - - let index = self.validate_index(index)?; - let chunk = data.hash_tree_root(); - let root = self.merkle_tree.push_and_compute_root(index, chunk); - let root_with_length = ssz::mix_in_length(root, index + 1); - - self.deposit_count += 1; - - Ok(root_with_length) - } - - pub fn extend_and_construct_proofs( - &mut self, - deposit_data: &[&DepositData], - deposit_indices: Range, - proof_indices: Range, - ) -> Result> { - ensure!( - deposit_indices.start <= proof_indices.start - && proof_indices.start < proof_indices.end - && proof_indices.end <= deposit_indices.end, - Error::InvalidIndexRanges { - deposit_indices, - proof_indices, - }, - ); - - Self::validate_index_fits(deposit_indices.end - 1)?; - self.validate_index_expected(deposit_indices.start)?; - - let deposit_indices = deposit_indices.start.try_into()?..deposit_indices.end.try_into()?; - let proof_indices = proof_indices.start.try_into()?..proof_indices.end.try_into()?; - - let data_count = deposit_data.len(); - let index_count = deposit_indices.len(); - - ensure!( - data_count == index_count, - Error::CountMismatch { - data_count, - index_count, - }, - ); - - let chunks = deposit_data - .iter() - .copied() - .map(DepositData::hash_tree_root); - - let deposit_data = proof_indices - .clone() - .map(|index| deposit_data[index - deposit_indices.start]) - .copied(); - - let deposits = self - .merkle_tree - .extend_and_construct_proofs(chunks, deposit_indices.clone(), proof_indices) - .zip_eq(deposit_data) - .map(|(proof, data)| Deposit { proof, data }) - .collect(); - - self.deposit_count = deposit_indices.end.try_into()?; - - Ok(deposits) - } - - fn validate_index(&self, index: DepositIndex) -> Result { - Self::validate_index_fits(index)?; - self.validate_index_expected(index)?; - index.try_into().map_err(Into::into) - } - - fn validate_index_fits(index: DepositIndex) -> Result<()> { - ensure!(index < MAX_DEPOSITS, Error::Full { index }); - Ok(()) - } - - fn validate_index_expected(&self, index: DepositIndex) -> Result<()> { - let expected = self.deposit_count; - let actual = index; - - ensure!( - actual == expected, - Error::UnexpectedIndex { expected, actual }, - ); - - Ok(()) - } -} - -#[derive(Debug, Error)] -enum Error { - #[error("attempted to add deposit with index {index} to full deposit tree")] - Full { index: DepositIndex }, - #[error("expected deposit with index {expected}, received deposit with index {actual}")] - UnexpectedIndex { - expected: DepositIndex, - actual: DepositIndex, - }, - #[error( - "index ranges are invalid \ - (deposit_indices: {deposit_indices:?}, proof_indices: {proof_indices:?})" - )] - InvalidIndexRanges { - deposit_indices: Range, - proof_indices: Range, - }, - #[error( - "deposit data count ({data_count}) does not match deposit index count ({index_count})" - )] - CountMismatch { - data_count: usize, - index_count: usize, - }, -} - -// False positive. See . -#[allow(clippy::range_plus_one)] -#[cfg(test)] -mod tests { - use spec_test_utils::Case; - use std_ext::ArcExt as _; - use test_generator::test_resources; - use types::{ - config::Config, - phase0::{containers::Eth1Data, primitives::ExecutionBlockHash}, - preset::{Minimal, Preset}, - traits::BeaconState as _, - }; - - use super::*; - - #[test] - fn push_fails_when_tree_is_full() { - let mut full_tree = DepositTree { - deposit_count: MAX_DEPOSITS, - ..DepositTree::default() - }; - - full_tree - .push(MAX_DEPOSITS, DepositData::default()) - .expect_err("pushing to a full tree should fail"); - } - - #[test] - fn push_fails_on_unexpected_index() { - let mut deposit_tree = DepositTree::default(); - - deposit_tree - .push(1, DepositData::default()) - .expect_err("pushing with incorrect index should fail"); - } - - #[test] - fn push_and_compute_root_fails_when_tree_is_full() { - let mut full_tree = DepositTree { - deposit_count: MAX_DEPOSITS, - ..DepositTree::default() - }; - - full_tree - .push_and_compute_root(MAX_DEPOSITS, DepositData::default()) - .expect_err("pushing to a full tree should fail"); - } - - #[test] - fn push_and_compute_root_fails_on_unexpected_index() { - let mut deposit_tree = DepositTree::default(); - - deposit_tree - .push_and_compute_root(1, DepositData::default()) - .expect_err("pushing with incorrect index should fail"); - } - - #[test] - fn extend_and_construct_proofs_fails_on_empty_ranges() { - let deposit_data = &[]; - let deposit_indices = 0..0; - let proof_indices = deposit_indices.clone(); - - let mut deposit_tree = DepositTree::default(); - - deposit_tree - .extend_and_construct_proofs(deposit_data, deposit_indices, proof_indices) - .expect_err("extending with empty ranges should fail"); - } - - #[test] - fn extend_and_construct_proofs_fails_when_tree_is_full() { - let deposit_data = &[&DepositData::default()]; - let deposit_indices = MAX_DEPOSITS..MAX_DEPOSITS + 1; - let proof_indices = deposit_indices.clone(); - - let mut full_tree = DepositTree { - deposit_count: MAX_DEPOSITS, - ..DepositTree::default() - }; - - full_tree - .extend_and_construct_proofs(deposit_data, deposit_indices, proof_indices) - .expect_err("extending a full tree should fail"); - } - - #[test] - fn extend_and_construct_proofs_fails_on_unexpected_index() { - let deposit_data = &[&DepositData::default()]; - let deposit_indices = 1..2; - let proof_indices = deposit_indices.clone(); - - let mut deposit_tree = DepositTree::default(); - - deposit_tree - .extend_and_construct_proofs(deposit_data, deposit_indices, proof_indices) - .expect_err("extending with incorrect index should fail"); - } - - // The tests based on `genesis/initialization` do not cover the multiple deposit case. - // Deposits processed during genesis (in `initialize_beacon_state_from_eth1`) are supposed to - // have proofs for the addition of each deposit individually. They do not contain hashes - // computed from later deposits. This may have been intended as an optimization for genesis. - // If the proofs included hashes computed from later deposits like they are supposed to after - // genesis, all of them would have to be updated for every new deposit. On the other hand, proof - // construction and verification can be avoided entirely during genesis, which is what we do in - // our implementation. - #[test] - fn extend_and_construct_proofs_handles_vote_for_multiple_deposits() -> Result<()> { - let config = Config::minimal(); - - let (mut state_0, deposit_tree_0) = factory::min_genesis_state::(&config)?; - - // Enough deposits to fill block #1 and leave one for block #2. - let block_0_count = state_0.eth1_deposit_index(); - let block_1_count = block_0_count + ::MaxDeposits::U64; - let block_2_count = block_1_count + 1; - - let new_deposit_data = (block_0_count..block_2_count) - .map(interop::secret_key) - .map(|secret_key| interop::quick_start_deposit_data::(&config, &secret_key)) - .collect_vec(); - - let deposit_root_2 = new_deposit_data - .iter() - .copied() - .zip_eq(block_0_count..block_2_count) - .scan(deposit_tree_0, |deposit_tree, (data, index)| { - Some(deposit_tree.push_and_compute_root(index, data)) - }) - .reduce(Result::and) - .into_iter() - .exactly_one()??; - - // Fake a successful `Eth1Data` vote for multiple new deposits. - *state_0.make_mut().eth1_data_mut() = Eth1Data { - deposit_root: deposit_root_2, - deposit_count: block_2_count, - block_hash: ExecutionBlockHash::default(), - }; - - let new_deposit_data = new_deposit_data.iter().collect_vec(); - let new_deposit_indices = block_0_count..block_2_count; - let block_1_proof_indices = block_0_count..block_1_count; - let block_2_proof_indices = block_1_count..block_2_count; - - let block_1_deposits = deposit_tree_0 - .clone() - .extend_and_construct_proofs( - new_deposit_data.as_slice(), - new_deposit_indices.clone(), - block_1_proof_indices, - )? - .try_into()?; - - let block_2_deposits = deposit_tree_0 - .clone() - .extend_and_construct_proofs( - new_deposit_data.as_slice(), - new_deposit_indices, - block_2_proof_indices, - )? - .try_into()?; - - let (_, state_1) = factory::block_with_deposits(&config, state_0, 1, block_1_deposits)?; - let (_, state_2) = factory::block_with_deposits(&config, state_1, 2, block_2_deposits)?; - - assert_eq!(state_2.eth1_deposit_index(), block_2_count); - - Ok(()) - } - - #[test_resources("consensus-spec-tests/tests/*/phase0/genesis/initialization/*/*")] - fn extend_and_construct_proofs_matches_proofs_in_genesis_initialization_tests(case: Case) { - let deposits_count = case.meta().deposits_count; - let deposits = case.numbered_default::("deposits", 0..deposits_count); - - let mut deposit_tree = DepositTree::default(); - - for (expected_deposit, deposit_index) in deposits.zip(0..) { - let deposit_data = &[&expected_deposit.data]; - let deposit_indices = deposit_index..deposit_index + 1; - let proof_indices = deposit_indices.clone(); - - let actual_deposit = deposit_tree - .extend_and_construct_proofs(deposit_data, deposit_indices, proof_indices) - .expect("deposits are not enough to fill tree and have correct indices") - .into_iter() - .exactly_one() - .expect("exactly one proof is requested"); - - assert_eq!(actual_deposit, expected_deposit); - } - } -} +pub use tree::DepositTree; +pub use eip_4881::snapshot::{DepositTreeSnapshot, FinalizedDeposit}; diff --git a/deposit_tree/src/tree.rs b/deposit_tree/src/tree.rs new file mode 100644 index 00000000..914f7b0f --- /dev/null +++ b/deposit_tree/src/tree.rs @@ -0,0 +1,373 @@ +use core::ops::Range; + +use anyhow::{ensure, Result}; +use itertools::Itertools as _; +use ssz::{MerkleTree, Ssz, SszHash as _}; +use thiserror::Error; +use typenum::Unsigned as _; +use types::phase0::{ + consts::DepositContractTreeDepth, + containers::{Deposit, DepositData}, + primitives::{DepositIndex, ExecutionBlockNumber, H256}, +}; + +const MAX_DEPOSITS: DepositIndex = 1 << DepositContractTreeDepth::USIZE; + +// We do not store the whole deposit tree, only hashes that are needed to construct proofs. +// These implementations appear to use the same algorithm: +// - +// - +// - +// +// See the [reference implementation in EIP-4881] for another approach. +// +// [reference implementation in EIP-4881]: https://eips.ethereum.org/EIPS/eip-4881#reference-implementation +#[derive(Clone, Copy, Default, Ssz)] +#[ssz(derive_hash = false)] +pub struct DepositTree { + pub merkle_tree: MerkleTree, + pub deposit_count: DepositIndex, + // Latest Eth1 block from which deposits were added to deposit tree + pub last_added_block_number: ExecutionBlockNumber, +} + +impl DepositTree { + pub fn push(&mut self, index: DepositIndex, data: DepositData) -> Result<()> { + features::log!( + DebugEth1, + "DepositTree::push (self.deposit_count: {}, index: {index}, data: {data:?})", + self.deposit_count, + ); + + let index = self.validate_index(index)?; + let chunk = data.hash_tree_root(); + + self.merkle_tree.push(index, chunk); + self.deposit_count += 1; + + Ok(()) + } + + pub fn push_and_compute_root( + &mut self, + index: DepositIndex, + data: DepositData, + ) -> Result { + features::log!( + DebugEth1, + "DepositTree::push_and_compute_root \ + (self.deposit_count: {}, index: {index}, data: {data:?})", + self.deposit_count, + ); + + let index = self.validate_index(index)?; + let chunk = data.hash_tree_root(); + let root = self.merkle_tree.push_and_compute_root(index, chunk); + let root_with_length = ssz::mix_in_length(root, index + 1); + + self.deposit_count += 1; + + Ok(root_with_length) + } + + pub fn extend_and_construct_proofs( + &mut self, + deposit_data: &[&DepositData], + deposit_indices: Range, + proof_indices: Range, + ) -> Result> { + ensure!( + deposit_indices.start <= proof_indices.start + && proof_indices.start < proof_indices.end + && proof_indices.end <= deposit_indices.end, + Error::InvalidIndexRanges { + deposit_indices, + proof_indices, + }, + ); + + Self::validate_index_fits(deposit_indices.end - 1)?; + self.validate_index_expected(deposit_indices.start)?; + + let deposit_indices = deposit_indices.start.try_into()?..deposit_indices.end.try_into()?; + let proof_indices = proof_indices.start.try_into()?..proof_indices.end.try_into()?; + + let data_count = deposit_data.len(); + let index_count = deposit_indices.len(); + + ensure!( + data_count == index_count, + Error::CountMismatch { + data_count, + index_count, + }, + ); + + let chunks = deposit_data + .iter() + .copied() + .map(DepositData::hash_tree_root); + + let deposit_data = proof_indices + .clone() + .map(|index| deposit_data[index - deposit_indices.start]) + .copied(); + + let deposits = self + .merkle_tree + .extend_and_construct_proofs(chunks, deposit_indices.clone(), proof_indices) + .zip_eq(deposit_data) + .map(|(proof, data)| Deposit { proof, data }) + .collect(); + + self.deposit_count = deposit_indices.end.try_into()?; + + Ok(deposits) + } + + fn validate_index(&self, index: DepositIndex) -> Result { + Self::validate_index_fits(index)?; + self.validate_index_expected(index)?; + index.try_into().map_err(Into::into) + } + + fn validate_index_fits(index: DepositIndex) -> Result<()> { + ensure!(index < MAX_DEPOSITS, Error::Full { index }); + Ok(()) + } + + fn validate_index_expected(&self, index: DepositIndex) -> Result<()> { + let expected = self.deposit_count; + let actual = index; + + ensure!( + actual == expected, + Error::UnexpectedIndex { expected, actual }, + ); + + Ok(()) + } +} + +#[derive(Debug, Error)] +enum Error { + #[error("attempted to add deposit with index {index} to full deposit tree")] + Full { index: DepositIndex }, + #[error("expected deposit with index {expected}, received deposit with index {actual}")] + UnexpectedIndex { + expected: DepositIndex, + actual: DepositIndex, + }, + #[error( + "index ranges are invalid \ + (deposit_indices: {deposit_indices:?}, proof_indices: {proof_indices:?})" + )] + InvalidIndexRanges { + deposit_indices: Range, + proof_indices: Range, + }, + #[error( + "deposit data count ({data_count}) does not match deposit index count ({index_count})" + )] + CountMismatch { + data_count: usize, + index_count: usize, + }, +} + +// False positive. See . +#[allow(clippy::range_plus_one)] +#[cfg(test)] +mod tests { + use spec_test_utils::Case; + use std_ext::ArcExt as _; + use test_generator::test_resources; + use types::{ + config::Config, + phase0::{containers::Eth1Data, primitives::ExecutionBlockHash}, + preset::{Minimal, Preset}, + traits::BeaconState as _, + }; + + use super::*; + + #[test] + fn push_fails_when_tree_is_full() { + let mut full_tree = DepositTree { + deposit_count: MAX_DEPOSITS, + ..DepositTree::default() + }; + + full_tree + .push(MAX_DEPOSITS, DepositData::default()) + .expect_err("pushing to a full tree should fail"); + } + + #[test] + fn push_fails_on_unexpected_index() { + let mut deposit_tree = DepositTree::default(); + + deposit_tree + .push(1, DepositData::default()) + .expect_err("pushing with incorrect index should fail"); + } + + #[test] + fn push_and_compute_root_fails_when_tree_is_full() { + let mut full_tree = DepositTree { + deposit_count: MAX_DEPOSITS, + ..DepositTree::default() + }; + + full_tree + .push_and_compute_root(MAX_DEPOSITS, DepositData::default()) + .expect_err("pushing to a full tree should fail"); + } + + #[test] + fn push_and_compute_root_fails_on_unexpected_index() { + let mut deposit_tree = DepositTree::default(); + + deposit_tree + .push_and_compute_root(1, DepositData::default()) + .expect_err("pushing with incorrect index should fail"); + } + + #[test] + fn extend_and_construct_proofs_fails_on_empty_ranges() { + let deposit_data = &[]; + let deposit_indices = 0..0; + let proof_indices = deposit_indices.clone(); + + let mut deposit_tree = DepositTree::default(); + + deposit_tree + .extend_and_construct_proofs(deposit_data, deposit_indices, proof_indices) + .expect_err("extending with empty ranges should fail"); + } + + #[test] + fn extend_and_construct_proofs_fails_when_tree_is_full() { + let deposit_data = &[&DepositData::default()]; + let deposit_indices = MAX_DEPOSITS..MAX_DEPOSITS + 1; + let proof_indices = deposit_indices.clone(); + + let mut full_tree = DepositTree { + deposit_count: MAX_DEPOSITS, + ..DepositTree::default() + }; + + full_tree + .extend_and_construct_proofs(deposit_data, deposit_indices, proof_indices) + .expect_err("extending a full tree should fail"); + } + + #[test] + fn extend_and_construct_proofs_fails_on_unexpected_index() { + let deposit_data = &[&DepositData::default()]; + let deposit_indices = 1..2; + let proof_indices = deposit_indices.clone(); + + let mut deposit_tree = DepositTree::default(); + + deposit_tree + .extend_and_construct_proofs(deposit_data, deposit_indices, proof_indices) + .expect_err("extending with incorrect index should fail"); + } + + // The tests based on `genesis/initialization` do not cover the multiple deposit case. + // Deposits processed during genesis (in `initialize_beacon_state_from_eth1`) are supposed to + // have proofs for the addition of each deposit individually. They do not contain hashes + // computed from later deposits. This may have been intended as an optimization for genesis. + // If the proofs included hashes computed from later deposits like they are supposed to after + // genesis, all of them would have to be updated for every new deposit. On the other hand, proof + // construction and verification can be avoided entirely during genesis, which is what we do in + // our implementation. + #[test] + fn extend_and_construct_proofs_handles_vote_for_multiple_deposits() -> Result<()> { + let config = Config::minimal(); + + let (mut state_0, deposit_tree_0) = factory::min_genesis_state::(&config)?; + + // Enough deposits to fill block #1 and leave one for block #2. + let block_0_count = state_0.eth1_deposit_index(); + let block_1_count = block_0_count + ::MaxDeposits::U64; + let block_2_count = block_1_count + 1; + + let new_deposit_data = (block_0_count..block_2_count) + .map(interop::secret_key) + .map(|secret_key| interop::quick_start_deposit_data::(&config, &secret_key)) + .collect_vec(); + + let deposit_root_2 = new_deposit_data + .iter() + .copied() + .zip_eq(block_0_count..block_2_count) + .scan(deposit_tree_0, |deposit_tree, (data, index)| { + Some(deposit_tree.push_and_compute_root(index, data)) + }) + .reduce(Result::and) + .into_iter() + .exactly_one()??; + + // Fake a successful `Eth1Data` vote for multiple new deposits. + *state_0.make_mut().eth1_data_mut() = Eth1Data { + deposit_root: deposit_root_2, + deposit_count: block_2_count, + block_hash: ExecutionBlockHash::default(), + }; + + let new_deposit_data = new_deposit_data.iter().collect_vec(); + let new_deposit_indices = block_0_count..block_2_count; + let block_1_proof_indices = block_0_count..block_1_count; + let block_2_proof_indices = block_1_count..block_2_count; + + let block_1_deposits = deposit_tree_0 + .clone() + .extend_and_construct_proofs( + new_deposit_data.as_slice(), + new_deposit_indices.clone(), + block_1_proof_indices, + )? + .try_into()?; + + let block_2_deposits = deposit_tree_0 + .clone() + .extend_and_construct_proofs( + new_deposit_data.as_slice(), + new_deposit_indices, + block_2_proof_indices, + )? + .try_into()?; + + let (_, state_1) = factory::block_with_deposits(&config, state_0, 1, block_1_deposits)?; + let (_, state_2) = factory::block_with_deposits(&config, state_1, 2, block_2_deposits)?; + + assert_eq!(state_2.eth1_deposit_index(), block_2_count); + + Ok(()) + } + + #[test_resources("consensus-spec-tests/tests/*/phase0/genesis/initialization/*/*")] + fn extend_and_construct_proofs_matches_proofs_in_genesis_initialization_tests(case: Case) { + let deposits_count = case.meta().deposits_count; + let deposits = case.numbered_default::("deposits", 0..deposits_count); + + let mut deposit_tree = DepositTree::default(); + + for (expected_deposit, deposit_index) in deposits.zip(0..) { + let deposit_data = &[&expected_deposit.data]; + let deposit_indices = deposit_index..deposit_index + 1; + let proof_indices = deposit_indices.clone(); + + let actual_deposit = deposit_tree + .extend_and_construct_proofs(deposit_data, deposit_indices, proof_indices) + .expect("deposits are not enough to fill tree and have correct indices") + .into_iter() + .exactly_one() + .expect("exactly one proof is requested"); + + assert_eq!(actual_deposit, expected_deposit); + } + } +} From 01493c92fbe677fb6b8c7dea895d2d70ce521a51 Mon Sep 17 00:00:00 2001 From: Sulpiride Date: Tue, 20 Aug 2024 23:53:57 +0500 Subject: [PATCH 2/5] Add eip4881 data structures --- Cargo.lock | 3 + deposit_tree/Cargo.toml | 3 + deposit_tree/src/eip_4881/deposit_tree.rs | 174 +++++++ deposit_tree/src/eip_4881/merkle_tree.rs | 594 ++++++++++++++++++++++ deposit_tree/src/eip_4881/mod.rs | 3 +- deposit_tree/src/eip_4881/snapshot.rs | 47 +- deposit_tree/src/eip_4881/tree.rs | 9 - deposit_tree/src/lib.rs | 2 + genesis/src/lib.rs | 16 +- 9 files changed, 820 insertions(+), 31 deletions(-) create mode 100644 deposit_tree/src/eip_4881/deposit_tree.rs create mode 100644 deposit_tree/src/eip_4881/merkle_tree.rs delete mode 100644 deposit_tree/src/eip_4881/tree.rs diff --git a/Cargo.lock b/Cargo.lock index 46466eea..f2bd9273 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1756,6 +1756,9 @@ dependencies = [ "hashing", "interop", "itertools 0.13.0", + "lazy_static", + "quickcheck", + "quickcheck_macros", "spec_test_utils", "ssz", "std_ext", diff --git a/deposit_tree/Cargo.toml b/deposit_tree/Cargo.toml index d16c2f47..a5138bac 100644 --- a/deposit_tree/Cargo.toml +++ b/deposit_tree/Cargo.toml @@ -15,6 +15,7 @@ thiserror = { workspace = true } typenum = { workspace = true } types = { workspace = true } hashing = { workspace = true } +lazy_static = { workspace = true } [dev-dependencies] factory = { workspace = true } @@ -22,3 +23,5 @@ interop = { workspace = true } spec_test_utils = { workspace = true } std_ext = { workspace = true } test-generator = { workspace = true } +quickcheck = { workspace = true } +quickcheck_macros = { workspace = true } diff --git a/deposit_tree/src/eip_4881/deposit_tree.rs b/deposit_tree/src/eip_4881/deposit_tree.rs new file mode 100644 index 00000000..812ecef6 --- /dev/null +++ b/deposit_tree/src/eip_4881/deposit_tree.rs @@ -0,0 +1,174 @@ +use std::ops::Add; +use core::ops::Range; + +use anyhow::{ensure, Result}; +use thiserror::Error; +use typenum::Unsigned as _; +use ssz::{mix_in_length, SszHash, H256}; +use types::phase0::{ + consts::DepositContractTreeDepth, + containers::DepositData, + primitives::DepositIndex +}; + +use crate::DepositTreeSnapshot; + +use super::{ + merkle_tree::{EIP4881MerkleTree, EIP4881MerkleTreeError}, + snapshot::FinalizedExecutionBlock +}; + +const MAX_DEPOSITS: DepositIndex = 1 << DepositContractTreeDepth::USIZE; + +#[derive(Clone, Default)] +pub struct DepositDataTree { + pub tree: EIP4881MerkleTree, + pub length: DepositIndex, + pub finalized_execution_block: Option, + pub depth: usize +} + +impl DepositDataTree { + #[must_use] + pub fn create(leaves: &[H256], length: DepositIndex, depth: usize) -> Self { + Self { + tree: EIP4881MerkleTree::create(leaves, depth), + length, + finalized_execution_block: None, + depth, + } + } + + /// Retrieve the root hash of this Merkle tree with the length mixed in. + #[must_use] + pub fn root(&self) -> H256 { + mix_in_length(self.tree.hash(), self.length as usize) + } + + /// Return the leaf at `index` and a Merkle proof of its inclusion. + /// + /// The Merkle proof is in "bottom-up" order, starting with a leaf node + /// and moving up the tree. Its length will be exactly equal to `depth + 1`. + pub fn generate_proof(&self, index: usize) -> Result<(H256, Vec), EIP4881MerkleTreeError> { + let (root, mut proof) = self.tree.generate_proof(index, self.depth)?; + proof.push(self.root()); + Ok((root, proof)) + } + + /// Add a deposit to the merkle tree. + pub fn push_leaf(&mut self, leaf: H256) -> Result<(), EIP4881MerkleTreeError> { + self.tree.push_leaf(leaf, self.depth)?; + self.length = self.length.add(1); + Ok(()) + } + + /// Finalize deposits up to `finalized_execution_block.deposit_count` + pub fn finalize( + &mut self, + finalized_execution_block: FinalizedExecutionBlock + ) -> Result<(), EIP4881MerkleTreeError> { + self.tree + .finalize_deposits(finalized_execution_block.deposit_count as usize, self.depth)?; + self.finalized_execution_block = Some(finalized_execution_block); + Ok(()) + } + + pub fn push_and_compute_root( + &mut self, + index: DepositIndex, + data: DepositData + ) -> Result { + features::log!( + DebugEth1, + "DepositDataTree::push_and_compute_root \ + (self.deposit_count: {}, index: {index}, data: {data:?})", + self.length, + ); + + self.validate_index(index)?; + let chunk = data.hash_tree_root(); + self.push_leaf(chunk); + return Ok(self.root()); + } + + /// Get snapshot of finalized deposit tree (if tree is finalized) + #[must_use] + pub fn get_snapshot(&self) -> Option { + let finalized_execution_block = self.finalized_execution_block.as_ref()?; + Some(DepositTreeSnapshot { + finalized: self.tree.get_finalized_hashes(), + execution_block: FinalizedExecutionBlock { + deposit_root: finalized_execution_block.deposit_root, + deposit_count: finalized_execution_block.deposit_count, + block_hash: finalized_execution_block.block_hash, + block_height: finalized_execution_block.block_height, + } + }) + } + + /// Create a new Merkle tree from a snapshot + pub fn from_snapshot( + snapshot: &DepositTreeSnapshot, + depth: usize, + ) -> Result { + Ok(Self { + tree: EIP4881MerkleTree::from_finalized_snapshot( + &snapshot.finalized.into_iter().map(|hash| *hash).collect(), + snapshot.execution_block.deposit_count as usize, + depth, + )?, + length: snapshot.execution_block.deposit_count, + finalized_execution_block: Some(snapshot.into()), + depth, + }) + } + + fn validate_index(&self, index: DepositIndex) -> Result { + Self::validate_index_fits(index)?; + self.validate_index_expected(index)?; + index.try_into().map_err(Into::into) + } + + fn validate_index_fits(index: DepositIndex) -> Result<()> { + ensure!(index < MAX_DEPOSITS, Error::Full { index }); + Ok(()) + } + + fn validate_index_expected(&self, index: DepositIndex) -> Result<()> { + let expected = self.length; + let actual = index; + + ensure!( + actual == expected, + Error::UnexpectedIndex { expected, actual }, + ); + + Ok(()) + } +} + +#[derive(Debug, Error)] +enum Error { + #[error("attempted to add deposit with index {index} to full deposit tree")] + Full { index: DepositIndex }, + #[error("expected deposit with index {expected}, received deposit with index {actual}")] + UnexpectedIndex { + expected: DepositIndex, + actual: DepositIndex, + }, + #[error( + "index ranges are invalid \ + (deposit_indices: {deposit_indices:?}, proof_indices: {proof_indices:?})" + )] + InvalidIndexRanges { + deposit_indices: Range, + proof_indices: Range, + }, + #[error( + "deposit data count ({data_count}) does not match deposit index count ({index_count})" + )] + CountMismatch { + data_count: usize, + index_count: usize, + }, +} diff --git a/deposit_tree/src/eip_4881/merkle_tree.rs b/deposit_tree/src/eip_4881/merkle_tree.rs new file mode 100644 index 00000000..93a1b5d7 --- /dev/null +++ b/deposit_tree/src/eip_4881/merkle_tree.rs @@ -0,0 +1,594 @@ +use hashing::{hash_256_256, ZERO_HASHES}; +use typenum::Unsigned; +use types::phase0::consts::DepositContractTreeDepth; +use ssz::H256; +use lazy_static::lazy_static; + +use crate::FinalizedDeposit; + +lazy_static! { + static ref ZERO_NODES: Vec = { + (0..=MAX_TREE_DEPTH).map(EIP4881MerkleTree::Zero).collect() + }; +} + +pub const MAX_TREE_DEPTH: usize = DepositContractTreeDepth::USIZE; +pub const EMPTY_SLICE: &[H256] = &[]; + +/// Right-sparse Merkle tree. +/// +/// Efficiently represents a Merkle tree of fixed depth where only the first N +/// indices are populated by non-zero leaves (perfect for the deposit contract tree). +#[derive(Debug, PartialEq, Clone)] +pub enum EIP4881MerkleTree { + Zero(usize), + Leaf(H256), + Node(H256, Box, Box), + Finalized(H256) +} + +impl Default for EIP4881MerkleTree { + fn default() -> Self { + Self::Zero(MAX_TREE_DEPTH) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum EIP4881MerkleTreeError { + // Trying to push in a leaf + LeafReached, + // No more space in the MerkleTree + MerkleTreeFull, + // MerkleTree is invalid + Invalid, + // Incorrect Depth provided + DepthTooSmall, + // Overflow occurred + ArithError, + // Can't finalize a zero node + ZeroNodeFinalized, + // Can't push to finalized node + FinalizedNodePushed, + // Invalid Snapshot + InvalidSnapshot(InvalidSnapshot), + // Can't proof a finalized node + ProofEncounteredFinalizedNode, + // This should never happen + PleaseNotifyTheDevs, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum InvalidSnapshot { + // Branch hashes are empty but deposits are not + EmptyBranchWithNonZeroDeposits(usize), + // End of tree reached but deposits != 1 + EndOfTree, +} + +impl EIP4881MerkleTree { + #[must_use] + pub fn create(leaves: &[H256], depth: usize) -> Self { + use EIP4881MerkleTree::{Leaf, Node, Zero}; + if leaves.is_empty() { + return Zero(depth); + } + + match depth { + 0 => { + debug_assert_eq!(leaves.len(), 1); + Leaf(leaves[0]) + }, + _ => { + let subtree_capacity = 2usize.pow(depth as u32 - 1); + let (left_leaves, right_leaves) = if leaves.len() <= subtree_capacity { + (leaves, EMPTY_SLICE) + } else { + leaves.split_at(subtree_capacity) + }; + + let left_subtree = Self::create(left_leaves, depth - 1); + let right_subtree = Self::create(right_leaves, depth - 1); + let hash = hash_256_256(left_subtree.hash(), right_subtree.hash()); + + Node(hash, Box::new(left_subtree), Box::new(right_subtree)) + } + } + } + + pub fn push_leaf(&mut self, elem: H256, depth: usize) -> Result<(), EIP4881MerkleTreeError> { + use EIP4881MerkleTree::{Finalized, Leaf, Node, Zero}; + + if depth == 0 { + return Err(EIP4881MerkleTreeError::DepthTooSmall); + } + + match self { + Leaf(_) => return Err(EIP4881MerkleTreeError::LeafReached), + Zero(_) => { + *self = Self::create(&[elem], depth) + } + Node(ref mut hash, ref mut left, ref mut right) => { + let left: &mut Self = &mut *left; + let right: &mut Self = &mut *right; + match (&*left, &*right) { + (Leaf(_) | Finalized(_), Leaf(_)) => { + return Err(EIP4881MerkleTreeError::MerkleTreeFull); + } + // There is a right node so insert in right node + (Node(_, _, _) | Finalized(_), Node(_, _, _)) => { + right.push_leaf(elem, depth - 1)?; + } + (Zero(_), Zero(_)) => { + *left = Self::create(&[elem], depth - 1); + } + (Leaf(_) | Finalized(_), Zero(_)) => { + *right = Self::create(&[elem], depth - 1); + } + // Try inserting on the left node -> if it fails because it is full, insert in right side. + (Node(_, _, _), Zero(_)) => { + match left.push_leaf(elem, depth - 1) { + Ok(()) => (), + // Left node is full, insert in right node + Err(EIP4881MerkleTreeError::MerkleTreeFull) => { + *right = Self::create(&[elem], depth - 1); + } + Err(e) => return Err(e), + }; + }, + (_, _) => return Err(EIP4881MerkleTreeError::Invalid), + } + *hash = hash_256_256(left.hash(), right.hash()); + }, + Finalized(_) => return Err(EIP4881MerkleTreeError::FinalizedNodePushed), + } + + Ok(()) + } + + #[must_use] + pub const fn hash(&self) -> H256 { + use EIP4881MerkleTree::{Finalized, Leaf, Node, Zero}; + match *self { + Zero(depth) => ZERO_HASHES[depth], + Leaf(hash) | Node(hash, _, _) | Finalized(hash) => hash, + } + } + + /// Get a reference to the left and right subtrees if they exist. + #[must_use] + pub fn left_and_right_branches(&self) -> Option<(&Self, &Self)> { + use EIP4881MerkleTree::{Finalized, Leaf, Node, Zero}; + match *self { + Finalized(_) | Leaf(_) | Zero(0) => None, + Node(_, ref l, ref r) => Some((l, r)), + Zero(depth) => Some((&ZERO_NODES[depth - 1], &ZERO_NODES[depth - 1])), + } + } + + /// Is this Merkle tree a leaf? + #[must_use] + pub const fn is_leaf(&self) -> bool { + matches!(self, Self::Leaf(_)) + } + + /// Finalize deposits up to deposit with count = deposits_to_finalize + pub fn finalize_deposits( + &mut self, + deposits_to_finalize: usize, + level: usize, + ) -> Result<(), EIP4881MerkleTreeError> { + use EIP4881MerkleTree::{Finalized, Leaf, Node, Zero}; + match self { + Finalized(_) => Ok(()), + Zero(_) => Err(EIP4881MerkleTreeError::ZeroNodeFinalized), + Leaf(hash) => { + if level != 0 { + // This shouldn't happen but this is a sanity check + return Err(EIP4881MerkleTreeError::PleaseNotifyTheDevs); + } + *self = Finalized(*hash); + Ok(()) + } + Node(hash, left, right) => { + if level == 0 { + // this shouldn't happen but we'll put it here for safety + return Err(EIP4881MerkleTreeError::PleaseNotifyTheDevs); + } + let deposits = 0x1 << level; + if deposits <= deposits_to_finalize { + *self = Self::Finalized(*hash); + return Ok(()); + } + left.finalize_deposits(deposits_to_finalize, level - 1)?; + if deposits_to_finalize > deposits / 2 { + let remaining = deposits_to_finalize - deposits / 2; + right.finalize_deposits(remaining, level - 1)?; + } + Ok(()) + } + } + } + + + fn append_finalized_hashes(&self, result: &mut FinalizedDeposit) { + match self { + Self::Zero(_) | Self::Leaf(_) => {} + Self::Finalized(h) => { result.push(*h); }, + Self::Node(_, left, right) => { + left.append_finalized_hashes(result); + right.append_finalized_hashes(result); + } + } + } + + #[must_use] + pub fn get_finalized_hashes(&self) -> FinalizedDeposit { + let mut result = FinalizedDeposit::default(); + self.append_finalized_hashes(&mut result); + result + } + + pub fn from_finalized_snapshot( + finalized_branch: &Vec, + deposit_count: usize, + level: usize, + ) -> Result { + if finalized_branch.is_empty() { + return if deposit_count == 0 { + Ok(Self::Zero(level)) + } else { + Err(InvalidSnapshot::EmptyBranchWithNonZeroDeposits(deposit_count).into()) + }; + } + + if deposit_count == (0x1 << level) { + return Ok(Self::Finalized( + *finalized_branch.first().ok_or(EIP4881MerkleTreeError::PleaseNotifyTheDevs)? + )); + } + if level == 0 { + return Err(InvalidSnapshot::EndOfTree.into()); + } + + let (left, right) = match deposit_count.checked_sub(0x1 << (level - 1)) { + // left tree is fully finalized + Some(right_deposits) => { + let (left_hash, right_branch) = finalized_branch + .split_first() + .ok_or(EIP4881MerkleTreeError::PleaseNotifyTheDevs)?; + ( + Self::Finalized(*left_hash), + Self::from_finalized_snapshot( + &right_branch.into(), + right_deposits, + level - 1 + )?, + ) + } + // left tree is not fully finalized -> right tree is zero + None => ( + Self::from_finalized_snapshot(finalized_branch, deposit_count, level - 1)?, + Self::Zero(level - 1), + ), + }; + + let hash = hash_256_256(left.hash(), right.hash()); + Ok(Self::Node(hash, Box::new(left), Box::new(right))) + } + + /// Return the leaf at `index` and a Merkle proof of its inclusion. + /// + /// The Merkle proof is in "bottom-up" order, starting with a leaf node + /// and moving up the tree. Its length will be exactly equal to `depth`. + pub fn generate_proof( + &self, + index: usize, + depth: usize, + ) -> Result<(H256, Vec), EIP4881MerkleTreeError> { + let mut proof = vec![]; + let mut current_node = self; + let mut current_depth = depth; + while current_depth > 0 { + let ith_bit = (index >> (current_depth - 1)) & 0x01; + if let &Self::Finalized(_) = current_node { + return Err(EIP4881MerkleTreeError::ProofEncounteredFinalizedNode); + } + // Note: unwrap is safe because leaves are only ever constructed at depth == 0. + let (left, right) = current_node.left_and_right_branches().unwrap(); + + // Go right, include the left branch in the proof. + if ith_bit == 1 { + proof.push(left.hash()); + current_node = right; + } else { + proof.push(right.hash()); + current_node = left; + } + current_depth -= 1; + } + + debug_assert_eq!(proof.len(), depth); + debug_assert!(current_node.is_leaf()); + + // Put proof in bottom-up order. + proof.reverse(); + + Ok((current_node.hash(), proof)) + } + + /// useful for debugging + pub fn print_node(&self, mut space: u32) { + const SPACES: u32 = 10; + space += SPACES; + let (pair, text) = match self { + Self::Node(hash, left, right) => (Some((left, right)), format!("Node({})", hash)), + Self::Leaf(hash) => (None, format!("Leaf({})", hash)), + Self::Zero(depth) => ( + None, + format!("Z[{}]({})", depth, ZERO_HASHES[*depth]), + ), + Self::Finalized(hash) => (None, format!("Finl({})", hash)), + }; + if let Some((_, right)) = pair { + right.print_node(space); + } + println!(); + for _i in SPACES..space { + print!(" "); + } + println!("{}", text); + if let Some((left, _)) = pair { + left.print_node(space); + } + } +} + +/// Verify a proof that `leaf` exists at `index` in a Merkle tree rooted at `root`. +/// +/// The `branch` argument is the main component of the proof: it should be a list of internal +/// node hashes such that the root can be reconstructed (in bottom-up order). +pub fn verify_merkle_proof( + leaf: H256, + branch: &[H256], + depth: usize, + index: usize, + root: H256, +) -> bool { + if branch.len() == depth { + merkle_root_from_branch(leaf, branch, depth, index) == root + } else { + false + } +} + +/// Compute a root hash from a leaf and a Merkle proof. +pub fn merkle_root_from_branch(leaf: H256, branch: &[H256], depth: usize, index: usize) -> H256 { + assert_eq!(branch.len(), depth, "proof length should equal depth"); + + let mut merkle_root = leaf.clone(); + + for (i, leaf) in branch.iter().enumerate().take(depth) { + let ith_bit = (index >> i) & 0x01; + if ith_bit == 1 { + merkle_root = hash_256_256(*leaf, merkle_root); + } else { + merkle_root = hash_256_256(merkle_root, *leaf); + } + } + + merkle_root +} + +impl From for EIP4881MerkleTreeError { + fn from(e: InvalidSnapshot) -> Self { + EIP4881MerkleTreeError::InvalidSnapshot(e) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use quickcheck::TestResult; + use quickcheck_macros::quickcheck; + + /// Check that we can: + /// 1. Build a MerkleTree from arbitrary leaves and an arbitrary depth. + /// 2. Generate valid proofs for all of the leaves of this MerkleTree. + #[quickcheck] + fn quickcheck_create_and_verify(int_leaves: Vec, depth: usize) -> TestResult { + if depth > MAX_TREE_DEPTH || int_leaves.len() > 2usize.pow(depth as u32) { + return TestResult::discard(); + } + + let leaves: Vec<_> = int_leaves.into_iter().map(H256::from_low_u64_be).collect(); + let merkle_tree = EIP4881MerkleTree::create(&leaves, depth); + let merkle_root = merkle_tree.hash(); + + let proofs_ok = (0..leaves.len()).all(|i| { + let (leaf, branch) = merkle_tree + .generate_proof(i, depth) + .expect("should generate proof"); + leaf == leaves[i] && verify_merkle_proof(leaf, &branch, depth, i, merkle_root) + }); + + TestResult::from_bool(proofs_ok) + } + + #[quickcheck] + fn quickcheck_push_leaf_and_verify(int_leaves: Vec, depth: usize) -> TestResult { + if depth == 0 || depth > MAX_TREE_DEPTH || int_leaves.len() > 2usize.pow(depth as u32) { + return TestResult::discard(); + } + + let leaves_iter = int_leaves.into_iter().map(H256::from_low_u64_be); + + let mut merkle_tree = EIP4881MerkleTree::create(&[], depth); + + let proofs_ok = leaves_iter.enumerate().all(|(i, leaf)| { + assert_eq!(merkle_tree.push_leaf(leaf, depth), Ok(())); + let (stored_leaf, branch) = merkle_tree + .generate_proof(i, depth) + .expect("should generate proof"); + stored_leaf == leaf && verify_merkle_proof(leaf, &branch, depth, i, merkle_tree.hash()) + }); + + TestResult::from_bool(proofs_ok) + } + + #[test] + fn sparse_zero_correct() { + let depth = 2; + let zero = H256::from([0x00; 32]); + let dense_tree = EIP4881MerkleTree::create(&[zero, zero, zero, zero], depth); + let sparse_tree = EIP4881MerkleTree::create(&[], depth); + assert_eq!(dense_tree.hash(), sparse_tree.hash()); + } + + #[test] + fn create_small_example() { + // Construct a small merkle tree manually and check that it's consistent with + // the MerkleTree type. + let leaf_b00 = H256::from([0xAA; 32]); + let leaf_b01 = H256::from([0xBB; 32]); + let leaf_b10 = H256::from([0xCC; 32]); + let leaf_b11 = H256::from([0xDD; 32]); + + let node_b0x = hash_256_256(leaf_b00, leaf_b01); + let node_b1x = hash_256_256(leaf_b10, leaf_b11); + + let root = hash_256_256(node_b0x, node_b1x); + + let tree = EIP4881MerkleTree::create(&[leaf_b00, leaf_b01, leaf_b10, leaf_b11], 2); + assert_eq!(tree.hash(), root); + } + + #[test] + fn verify_small_example() { + // Construct a small merkle tree manually + let leaf_b00 = H256::from([0xAA; 32]); + let leaf_b01 = H256::from([0xBB; 32]); + let leaf_b10 = H256::from([0xCC; 32]); + let leaf_b11 = H256::from([0xDD; 32]); + + let node_b0x = hash_256_256(leaf_b00, leaf_b01); + let node_b1x = hash_256_256(leaf_b10, leaf_b11); + + let root = hash_256_256(node_b0x, node_b1x); + + // Run some proofs + assert!(verify_merkle_proof( + leaf_b00, + &[leaf_b01, node_b1x], + 2, + 0b00, + root + )); + assert!(verify_merkle_proof( + leaf_b01, + &[leaf_b00, node_b1x], + 2, + 0b01, + root + )); + assert!(verify_merkle_proof( + leaf_b10, + &[leaf_b11, node_b0x], + 2, + 0b10, + root + )); + assert!(verify_merkle_proof( + leaf_b11, + &[leaf_b10, node_b0x], + 2, + 0b11, + root + )); + assert!(verify_merkle_proof( + leaf_b11, + &[leaf_b10], + 1, + 0b11, + node_b1x + )); + + // Ensure that incorrect proofs fail + // Zero-length proof + assert!(!verify_merkle_proof(leaf_b01, &[], 2, 0b01, root)); + // Proof in reverse order + assert!(!verify_merkle_proof( + leaf_b01, + &[node_b1x, leaf_b00], + 2, + 0b01, + root + )); + // Proof too short + assert!(!verify_merkle_proof(leaf_b01, &[leaf_b00], 2, 0b01, root)); + // Wrong index + assert!(!verify_merkle_proof( + leaf_b01, + &[leaf_b00, node_b1x], + 2, + 0b10, + root + )); + // Wrong root + assert!(!verify_merkle_proof( + leaf_b01, + &[leaf_b00, node_b1x], + 2, + 0b01, + node_b1x + )); + } + + #[test] + fn verify_zero_depth() { + let leaf = H256::from([0xD6; 32]); + let junk = H256::from([0xD7; 32]); + assert!(verify_merkle_proof(leaf, &[], 0, 0, leaf)); + assert!(!verify_merkle_proof(leaf, &[], 0, 7, junk)); + } + + #[test] + fn push_complete_example() { + let depth = 2; + let mut tree = EIP4881MerkleTree::create(&[], depth); + + let leaf_b00 = H256::from([0xAA; 32]); + + let res = tree.push_leaf(leaf_b00, 0); + assert_eq!(res, Err(EIP4881MerkleTreeError::DepthTooSmall)); + let expected_tree = EIP4881MerkleTree::create(&[], depth); + assert_eq!(tree.hash(), expected_tree.hash()); + + tree.push_leaf(leaf_b00, depth) + .expect("Pushing in empty tree failed"); + let expected_tree = EIP4881MerkleTree::create(&[leaf_b00], depth); + assert_eq!(tree.hash(), expected_tree.hash()); + + let leaf_b01 = H256::from([0xBB; 32]); + tree.push_leaf(leaf_b01, depth) + .expect("Pushing in left then right node failed"); + let expected_tree = EIP4881MerkleTree::create(&[leaf_b00, leaf_b01], depth); + assert_eq!(tree.hash(), expected_tree.hash()); + + let leaf_b10 = H256::from([0xCC; 32]); + tree.push_leaf(leaf_b10, depth) + .expect("Pushing in right then left node failed"); + let expected_tree = EIP4881MerkleTree::create(&[leaf_b00, leaf_b01, leaf_b10], depth); + assert_eq!(tree.hash(), expected_tree.hash()); + + let leaf_b11 = H256::from([0xDD; 32]); + tree.push_leaf(leaf_b11, depth) + .expect("Pushing in outtermost leaf failed"); + let expected_tree = EIP4881MerkleTree::create(&[leaf_b00, leaf_b01, leaf_b10, leaf_b11], depth); + assert_eq!(tree.hash(), expected_tree.hash()); + + let leaf_b12 = H256::from([0xEE; 32]); + let res = tree.push_leaf(leaf_b12, depth); + assert_eq!(res, Err(EIP4881MerkleTreeError::MerkleTreeFull)); + assert_eq!(tree.hash(), expected_tree.hash()); + } +} \ No newline at end of file diff --git a/deposit_tree/src/eip_4881/mod.rs b/deposit_tree/src/eip_4881/mod.rs index 27140b9b..61b20f88 100644 --- a/deposit_tree/src/eip_4881/mod.rs +++ b/deposit_tree/src/eip_4881/mod.rs @@ -1,2 +1,3 @@ pub mod snapshot; -pub mod tree; +pub mod deposit_tree; +pub mod merkle_tree; diff --git a/deposit_tree/src/eip_4881/snapshot.rs b/deposit_tree/src/eip_4881/snapshot.rs index 8221556a..29ee1d4d 100644 --- a/deposit_tree/src/eip_4881/snapshot.rs +++ b/deposit_tree/src/eip_4881/snapshot.rs @@ -8,24 +8,28 @@ use hashing::hash_256_256; pub type FinalizedDeposit = PersistentList; +#[derive(Clone, Default, Ssz, Copy, PartialEq)] +pub struct FinalizedExecutionBlock { + pub deposit_root: H256, + pub deposit_count: DepositIndex, + pub block_hash: H256, + pub block_height: ExecutionBlockNumber, +} + // This is an implementation of a deposit tree snapshot described in EIP-4881 // ref: https://eips.ethereum.org/EIPS/eip-4881#reference-implementation -#[derive(Clone, Default, Ssz)] +#[derive(Clone, Default, Ssz, PartialEq)] #[ssz(derive_hash = false)] pub struct DepositTreeSnapshot { // proof of the latest finalized deposit - finalized: FinalizedDeposit, - // same as Eth1Data - deposit_root: H256, - deposit_count: DepositIndex, - execution_block_hash: H256, - execution_block_height: ExecutionBlockNumber, + pub finalized: FinalizedDeposit, + pub execution_block: FinalizedExecutionBlock, } impl DepositTreeSnapshot { #[must_use] pub fn calculate_root(&self) -> H256 { - let mut size = self.deposit_count; + let mut size = self.execution_block.deposit_count; let mut index = self.finalized.len_u64(); let mut root = ZERO_HASHES[0]; ZERO_HASHES @@ -41,7 +45,7 @@ impl DepositTreeSnapshot { } size >>= 1; }); - hash_256_256(root, H256::from_slice(&self.deposit_count.to_le_bytes())) + hash_256_256(root, H256::from_slice(&self.execution_block.deposit_count.to_le_bytes())) } #[must_use] @@ -50,14 +54,27 @@ impl DepositTreeSnapshot { deposit_count: DepositIndex, execution_block: (H256, ExecutionBlockNumber) ) -> Self { - let mut snapshot = DepositTreeSnapshot { + let mut snapshot = Self { finalized, - deposit_root: ZERO_HASHES[0], - deposit_count, - execution_block_hash: execution_block.0, - execution_block_height: execution_block.1 + execution_block: FinalizedExecutionBlock { + deposit_root: ZERO_HASHES[0], + deposit_count, + block_hash: execution_block.0, + block_height: execution_block.1 + } }; - snapshot.deposit_root = snapshot.calculate_root(); + snapshot.execution_block.deposit_root = snapshot.calculate_root(); snapshot } +} + +impl From<&DepositTreeSnapshot> for FinalizedExecutionBlock { + fn from(snapshot: &DepositTreeSnapshot) -> Self { + Self { + deposit_root: snapshot.execution_block.deposit_root, + deposit_count: snapshot.execution_block.deposit_count, + block_hash: snapshot.execution_block.block_hash, + block_height: snapshot.execution_block.block_height, + } + } } \ No newline at end of file diff --git a/deposit_tree/src/eip_4881/tree.rs b/deposit_tree/src/eip_4881/tree.rs deleted file mode 100644 index be282397..00000000 --- a/deposit_tree/src/eip_4881/tree.rs +++ /dev/null @@ -1,9 +0,0 @@ -use ssz::MerkleTree; -use types::phase0::{consts::DepositContractTreeDepth, containers::Eth1Data, primitives::ExecutionBlockNumber}; - -use crate::DepositTreeSnapshot; - -pub struct DepositTree { - pub merkle_tree: MerkleTree, - pub mix_in_length: u64, -} \ No newline at end of file diff --git a/deposit_tree/src/lib.rs b/deposit_tree/src/lib.rs index 7a8d022f..21475b7d 100644 --- a/deposit_tree/src/lib.rs +++ b/deposit_tree/src/lib.rs @@ -2,4 +2,6 @@ pub mod tree; pub mod eip_4881; pub use tree::DepositTree; +pub use eip_4881::deposit_tree::DepositDataTree; +pub use eip_4881::merkle_tree::{EIP4881MerkleTree, EIP4881MerkleTreeError, MAX_TREE_DEPTH, EMPTY_SLICE}; pub use eip_4881::snapshot::{DepositTreeSnapshot, FinalizedDeposit}; diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 0ef5e2a2..e10dcec6 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::{ensure, Result}; use arithmetic::U64Ext as _; -use deposit_tree::DepositTree; +use deposit_tree::{DepositDataTree, EMPTY_SLICE, MAX_TREE_DEPTH}; use helper_functions::accessors; use ssz::{PersistentVector, SszHash as _}; use std_ext::ArcExt as _; @@ -35,7 +35,7 @@ use types::{ nonstandard::{FinalizedCheckpoint, Phase, RelativeEpoch, WithOrigin}, phase0::{ beacon_state::BeaconState as Phase0BeaconState, - consts::{GENESIS_EPOCH, GENESIS_SLOT}, + consts::{DepositContractTreeDepth, GENESIS_EPOCH, GENESIS_SLOT}, containers::{ BeaconBlock as Phase0BeaconBlock, BeaconBlockBody as Phase0BeaconBlockBody, BeaconBlockHeader, DepositData, Fork, @@ -49,7 +49,7 @@ use types::{ pub struct Incremental<'config, P: Preset> { config: &'config Config, beacon_state: BeaconState

, - deposit_tree: DepositTree, + deposit_tree: DepositDataTree, } impl<'config, P: Preset> Incremental<'config, P> { @@ -121,7 +121,11 @@ impl<'config, P: Preset> Incremental<'config, P> { Self { config, beacon_state, - deposit_tree: DepositTree::default(), + deposit_tree: DepositDataTree::create( + EMPTY_SLICE, + 0 as DepositIndex, + MAX_TREE_DEPTH + ), } } @@ -144,7 +148,7 @@ impl<'config, P: Preset> Incremental<'config, P> { .deposit_tree .push_and_compute_root(deposit_index, data)?; - eth1_data.deposit_count = self.deposit_tree.deposit_count; + eth1_data.deposit_count = self.deposit_tree.length; // See . if let Some(validator_index) = @@ -174,7 +178,7 @@ impl<'config, P: Preset> Incremental<'config, P> { self, eth1_block_hash: ExecutionBlockHash, execution_payload_header: Option>, - ) -> Result<(BeaconState

, DepositTree)> { + ) -> Result<(BeaconState

, DepositDataTree)> { let Self { mut beacon_state, deposit_tree, From 0c2ba5dffd6d0f914fc536c3b461587e03390548 Mon Sep 17 00:00:00 2001 From: Sulpiride Date: Wed, 21 Aug 2024 23:37:19 +0500 Subject: [PATCH 3/5] wip: refactor & ssz EIP4881MerkleTree --- Cargo.lock | 1 + deposit_tree/Cargo.toml | 1 + deposit_tree/src/eip_4881/deposit_tree.rs | 34 ++++++++------ deposit_tree/src/eip_4881/merkle_tree.rs | 55 ++++++++++++++--------- eth1/src/eth1_cache.rs | 10 ++--- genesis/src/lib.rs | 2 +- interop/src/lib.rs | 4 +- 7 files changed, 64 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2bd9273..cc5c900f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1759,6 +1759,7 @@ dependencies = [ "lazy_static", "quickcheck", "quickcheck_macros", + "serde", "spec_test_utils", "ssz", "std_ext", diff --git a/deposit_tree/Cargo.toml b/deposit_tree/Cargo.toml index a5138bac..d88e87ee 100644 --- a/deposit_tree/Cargo.toml +++ b/deposit_tree/Cargo.toml @@ -16,6 +16,7 @@ typenum = { workspace = true } types = { workspace = true } hashing = { workspace = true } lazy_static = { workspace = true } +serde = { workspace = true } [dev-dependencies] factory = { workspace = true } diff --git a/deposit_tree/src/eip_4881/deposit_tree.rs b/deposit_tree/src/eip_4881/deposit_tree.rs index 812ecef6..3a8b4595 100644 --- a/deposit_tree/src/eip_4881/deposit_tree.rs +++ b/deposit_tree/src/eip_4881/deposit_tree.rs @@ -1,10 +1,11 @@ use std::ops::Add; use core::ops::Range; +use serde::{Deserialize, Serialize}; use anyhow::{ensure, Result}; use thiserror::Error; use typenum::Unsigned as _; -use ssz::{mix_in_length, SszHash, H256}; +use ssz::{mix_in_length, Ssz, SszHash, SszSize, H256}; use types::phase0::{ consts::DepositContractTreeDepth, containers::DepositData, @@ -25,12 +26,12 @@ pub struct DepositDataTree { pub tree: EIP4881MerkleTree, pub length: DepositIndex, pub finalized_execution_block: Option, - pub depth: usize + pub depth: u32 } impl DepositDataTree { #[must_use] - pub fn create(leaves: &[H256], length: DepositIndex, depth: usize) -> Self { + pub fn create(leaves: &[H256], length: DepositIndex, depth: u32) -> Self { Self { tree: EIP4881MerkleTree::create(leaves, depth), length, @@ -49,7 +50,7 @@ impl DepositDataTree { /// /// The Merkle proof is in "bottom-up" order, starting with a leaf node /// and moving up the tree. Its length will be exactly equal to `depth + 1`. - pub fn generate_proof(&self, index: usize) -> Result<(H256, Vec), EIP4881MerkleTreeError> { + pub fn generate_proof(&self, index: DepositIndex) -> Result<(H256, Vec), EIP4881MerkleTreeError> { let (root, mut proof) = self.tree.generate_proof(index, self.depth)?; proof.push(self.root()); Ok((root, proof)) @@ -68,16 +69,12 @@ impl DepositDataTree { finalized_execution_block: FinalizedExecutionBlock ) -> Result<(), EIP4881MerkleTreeError> { self.tree - .finalize_deposits(finalized_execution_block.deposit_count as usize, self.depth)?; + .finalize_deposits(finalized_execution_block.deposit_count, self.depth)?; self.finalized_execution_block = Some(finalized_execution_block); Ok(()) } - pub fn push_and_compute_root( - &mut self, - index: DepositIndex, - data: DepositData - ) -> Result { + pub fn push(&mut self, index: DepositIndex, data: DepositData) -> Result { features::log!( DebugEth1, "DepositDataTree::push_and_compute_root \ @@ -87,8 +84,17 @@ impl DepositDataTree { self.validate_index(index)?; let chunk = data.hash_tree_root(); - self.push_leaf(chunk); - return Ok(self.root()); + let _ = self.push_leaf(chunk); + Ok(self.root()) + } + + pub fn push_and_compute_root( + &mut self, + index: DepositIndex, + data: DepositData + ) -> Result { + self.push(index, data)?; + Ok(self.root()) } /// Get snapshot of finalized deposit tree (if tree is finalized) @@ -109,12 +115,12 @@ impl DepositDataTree { /// Create a new Merkle tree from a snapshot pub fn from_snapshot( snapshot: &DepositTreeSnapshot, - depth: usize, + depth: u32, ) -> Result { Ok(Self { tree: EIP4881MerkleTree::from_finalized_snapshot( &snapshot.finalized.into_iter().map(|hash| *hash).collect(), - snapshot.execution_block.deposit_count as usize, + snapshot.execution_block.deposit_count, depth, )?, length: snapshot.execution_block.deposit_count, diff --git a/deposit_tree/src/eip_4881/merkle_tree.rs b/deposit_tree/src/eip_4881/merkle_tree.rs index 93a1b5d7..f6fb92b9 100644 --- a/deposit_tree/src/eip_4881/merkle_tree.rs +++ b/deposit_tree/src/eip_4881/merkle_tree.rs @@ -1,18 +1,18 @@ use hashing::{hash_256_256, ZERO_HASHES}; use typenum::Unsigned; -use types::phase0::consts::DepositContractTreeDepth; -use ssz::H256; +use types::phase0::{consts::DepositContractTreeDepth, primitives::DepositIndex}; +use ssz::{Size, SszHash, SszSize, SszWrite, WriteError, H256}; use lazy_static::lazy_static; use crate::FinalizedDeposit; lazy_static! { static ref ZERO_NODES: Vec = { - (0..=MAX_TREE_DEPTH).map(EIP4881MerkleTree::Zero).collect() + (0..=MAX_TREE_DEPTH).map(|n| EIP4881MerkleTree::Zero(n as u32)).collect() }; } -pub const MAX_TREE_DEPTH: usize = DepositContractTreeDepth::USIZE; +pub const MAX_TREE_DEPTH: u32 = DepositContractTreeDepth::USIZE as u32; pub const EMPTY_SLICE: &[H256] = &[]; /// Right-sparse Merkle tree. @@ -21,12 +21,25 @@ pub const EMPTY_SLICE: &[H256] = &[]; /// indices are populated by non-zero leaves (perfect for the deposit contract tree). #[derive(Debug, PartialEq, Clone)] pub enum EIP4881MerkleTree { - Zero(usize), + Zero(u32), Leaf(H256), Node(H256, Box, Box), Finalized(H256) } +impl SszSize for EIP4881MerkleTree { + const SIZE: Size = Size::for_untagged_union([ + u32::SIZE, + H256::SIZE, + H256::SIZE, + H256::SIZE + ]); +} + +impl SszWrite for EIP4881MerkleTree { + +} + impl Default for EIP4881MerkleTree { fn default() -> Self { Self::Zero(MAX_TREE_DEPTH) @@ -60,14 +73,14 @@ pub enum EIP4881MerkleTreeError { #[derive(Debug, PartialEq, Clone)] pub enum InvalidSnapshot { // Branch hashes are empty but deposits are not - EmptyBranchWithNonZeroDeposits(usize), + EmptyBranchWithNonZeroDeposits(DepositIndex), // End of tree reached but deposits != 1 EndOfTree, } impl EIP4881MerkleTree { #[must_use] - pub fn create(leaves: &[H256], depth: usize) -> Self { + pub fn create(leaves: &[H256], depth: u32) -> Self { use EIP4881MerkleTree::{Leaf, Node, Zero}; if leaves.is_empty() { return Zero(depth); @@ -79,11 +92,11 @@ impl EIP4881MerkleTree { Leaf(leaves[0]) }, _ => { - let subtree_capacity = 2usize.pow(depth as u32 - 1); - let (left_leaves, right_leaves) = if leaves.len() <= subtree_capacity { + let subtree_capacity = 2u32.pow(depth - 1); + let (left_leaves, right_leaves) = if leaves.len() as u32 <= subtree_capacity { (leaves, EMPTY_SLICE) } else { - leaves.split_at(subtree_capacity) + leaves.split_at(subtree_capacity as usize) }; let left_subtree = Self::create(left_leaves, depth - 1); @@ -95,7 +108,7 @@ impl EIP4881MerkleTree { } } - pub fn push_leaf(&mut self, elem: H256, depth: usize) -> Result<(), EIP4881MerkleTreeError> { + pub fn push_leaf(&mut self, elem: H256, depth: u32) -> Result<(), EIP4881MerkleTreeError> { use EIP4881MerkleTree::{Finalized, Leaf, Node, Zero}; if depth == 0 { @@ -149,7 +162,7 @@ impl EIP4881MerkleTree { pub const fn hash(&self) -> H256 { use EIP4881MerkleTree::{Finalized, Leaf, Node, Zero}; match *self { - Zero(depth) => ZERO_HASHES[depth], + Zero(depth) => ZERO_HASHES[depth as usize], Leaf(hash) | Node(hash, _, _) | Finalized(hash) => hash, } } @@ -161,7 +174,7 @@ impl EIP4881MerkleTree { match *self { Finalized(_) | Leaf(_) | Zero(0) => None, Node(_, ref l, ref r) => Some((l, r)), - Zero(depth) => Some((&ZERO_NODES[depth - 1], &ZERO_NODES[depth - 1])), + Zero(depth) => Some((&ZERO_NODES[depth as usize - 1], &ZERO_NODES[depth as usize - 1])), } } @@ -174,8 +187,8 @@ impl EIP4881MerkleTree { /// Finalize deposits up to deposit with count = deposits_to_finalize pub fn finalize_deposits( &mut self, - deposits_to_finalize: usize, - level: usize, + deposits_to_finalize: DepositIndex, + level: u32, ) -> Result<(), EIP4881MerkleTreeError> { use EIP4881MerkleTree::{Finalized, Leaf, Node, Zero}; match self { @@ -230,8 +243,8 @@ impl EIP4881MerkleTree { pub fn from_finalized_snapshot( finalized_branch: &Vec, - deposit_count: usize, - level: usize, + deposit_count: DepositIndex, + level: u32, ) -> Result { if finalized_branch.is_empty() { return if deposit_count == 0 { @@ -282,8 +295,8 @@ impl EIP4881MerkleTree { /// and moving up the tree. Its length will be exactly equal to `depth`. pub fn generate_proof( &self, - index: usize, - depth: usize, + index: DepositIndex, + depth: u32, ) -> Result<(H256, Vec), EIP4881MerkleTreeError> { let mut proof = vec![]; let mut current_node = self; @@ -307,7 +320,7 @@ impl EIP4881MerkleTree { current_depth -= 1; } - debug_assert_eq!(proof.len(), depth); + debug_assert_eq!(proof.len() as u32, depth); debug_assert!(current_node.is_leaf()); // Put proof in bottom-up order. @@ -325,7 +338,7 @@ impl EIP4881MerkleTree { Self::Leaf(hash) => (None, format!("Leaf({})", hash)), Self::Zero(depth) => ( None, - format!("Z[{}]({})", depth, ZERO_HASHES[*depth]), + format!("Z[{}]({})", depth, ZERO_HASHES[*depth as usize]), ), Self::Finalized(hash) => (None, format!("Finl({})", hash)), }; diff --git a/eth1/src/eth1_cache.rs b/eth1/src/eth1_cache.rs index 40a92218..bbf60211 100644 --- a/eth1/src/eth1_cache.rs +++ b/eth1/src/eth1_cache.rs @@ -2,7 +2,7 @@ use std::sync::Mutex; use anyhow::Result; use database::Database; -use deposit_tree::DepositTree; +use deposit_tree::DepositDataTree; use eth1_api::{DepositEvent, Eth1Block}; use itertools::Itertools as _; use ssz::{SszReadDefault, SszWrite as _}; @@ -17,7 +17,7 @@ pub struct Eth1Cache { } impl Eth1Cache { - pub fn new(database: Database, default_deposit_tree: Option) -> Result { + pub fn new(database: Database, default_deposit_tree: Option) -> Result { let default_deposit_tree = default_deposit_tree.unwrap_or_default(); let deposit_tree = if let Some(tree) = get(&database, DEPOSIT_TREE_KEY)? { @@ -75,7 +75,7 @@ impl Eth1Cache { })? } - pub fn get_deposit_tree(&self) -> Result> { + pub fn get_deposit_tree(&self) -> Result> { get(&self.database, DEPOSIT_TREE_KEY) } @@ -99,7 +99,7 @@ impl Eth1Cache { .and_then(core::convert::identity) } - pub fn put_deposit_tree(&self, deposit_tree: &DepositTree) -> Result<()> { + pub fn put_deposit_tree(&self, deposit_tree: &DepositDataTree) -> Result<()> { put_deposit_tree(&self.database, deposit_tree) } } @@ -117,7 +117,7 @@ fn get(database: &Database, key: impl AsRef<[u8]>) -> Result< Ok(Some(value)) } -fn put_deposit_tree(database: &Database, deposit_tree: &DepositTree) -> Result<()> { +fn put_deposit_tree(database: &Database, deposit_tree: &DepositDataTree) -> Result<()> { database.put(DEPOSIT_TREE_KEY, deposit_tree.to_ssz()?) } diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index e10dcec6..ca2472fc 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -35,7 +35,7 @@ use types::{ nonstandard::{FinalizedCheckpoint, Phase, RelativeEpoch, WithOrigin}, phase0::{ beacon_state::BeaconState as Phase0BeaconState, - consts::{DepositContractTreeDepth, GENESIS_EPOCH, GENESIS_SLOT}, + consts::{GENESIS_EPOCH, GENESIS_SLOT}, containers::{ BeaconBlock as Phase0BeaconBlock, BeaconBlockBody as Phase0BeaconBlockBody, BeaconBlockHeader, DepositData, Fork, diff --git a/interop/src/lib.rs b/interop/src/lib.rs index 4c9a7cc6..34393e7e 100644 --- a/interop/src/lib.rs +++ b/interop/src/lib.rs @@ -2,7 +2,7 @@ use core::num::NonZeroU64; use anyhow::Result; use bls::{SecretKey, SecretKeyBytes}; -use deposit_tree::DepositTree; +use deposit_tree::DepositDataTree; use genesis::Incremental; use helper_functions::{misc, signing::SignForAllForks}; use hex_literal::hex; @@ -39,7 +39,7 @@ pub fn quick_start_beacon_state( config: &Config, genesis_time: UnixSeconds, validator_count: NonZeroU64, -) -> Result<(CombinedBeaconState

, DepositTree)> { +) -> Result<(CombinedBeaconState

, DepositDataTree)> { let mut incremental = Incremental::new(config); incremental.set_eth1_timestamp(QUICK_START_ETH1_BLOCK_TIMESTAMP); From 49187187c3bbe324564eff3aa2e5d04ad6cbc079 Mon Sep 17 00:00:00 2001 From: Sulpiride Date: Sun, 25 Aug 2024 00:16:32 +0500 Subject: [PATCH 4/5] wip: ssz for deposit data tree --- deposit_tree/src/eip_4881/deposit_tree.rs | 15 ++- deposit_tree/src/eip_4881/merkle_tree.rs | 122 +++++++++++++++++++--- 2 files changed, 114 insertions(+), 23 deletions(-) diff --git a/deposit_tree/src/eip_4881/deposit_tree.rs b/deposit_tree/src/eip_4881/deposit_tree.rs index 3a8b4595..892300d3 100644 --- a/deposit_tree/src/eip_4881/deposit_tree.rs +++ b/deposit_tree/src/eip_4881/deposit_tree.rs @@ -1,11 +1,10 @@ use std::ops::Add; use core::ops::Range; -use serde::{Deserialize, Serialize}; use anyhow::{ensure, Result}; use thiserror::Error; use typenum::Unsigned as _; -use ssz::{mix_in_length, Ssz, SszHash, SszSize, H256}; +use ssz::{mix_in_length, Ssz, SszHash, H256}; use types::phase0::{ consts::DepositContractTreeDepth, containers::DepositData, @@ -21,11 +20,11 @@ use super::{ const MAX_DEPOSITS: DepositIndex = 1 << DepositContractTreeDepth::USIZE; -#[derive(Clone, Default)] +#[derive(Clone, Default, Ssz)] pub struct DepositDataTree { pub tree: EIP4881MerkleTree, pub length: DepositIndex, - pub finalized_execution_block: Option, + pub finalized_execution_block: FinalizedExecutionBlock, pub depth: u32 } @@ -35,7 +34,7 @@ impl DepositDataTree { Self { tree: EIP4881MerkleTree::create(leaves, depth), length, - finalized_execution_block: None, + finalized_execution_block: FinalizedExecutionBlock::default(), depth, } } @@ -70,7 +69,7 @@ impl DepositDataTree { ) -> Result<(), EIP4881MerkleTreeError> { self.tree .finalize_deposits(finalized_execution_block.deposit_count, self.depth)?; - self.finalized_execution_block = Some(finalized_execution_block); + self.finalized_execution_block = finalized_execution_block; Ok(()) } @@ -100,7 +99,7 @@ impl DepositDataTree { /// Get snapshot of finalized deposit tree (if tree is finalized) #[must_use] pub fn get_snapshot(&self) -> Option { - let finalized_execution_block = self.finalized_execution_block.as_ref()?; + let finalized_execution_block = self.finalized_execution_block; Some(DepositTreeSnapshot { finalized: self.tree.get_finalized_hashes(), execution_block: FinalizedExecutionBlock { @@ -124,7 +123,7 @@ impl DepositDataTree { depth, )?, length: snapshot.execution_block.deposit_count, - finalized_execution_block: Some(snapshot.into()), + finalized_execution_block: snapshot.into(), depth, }) } diff --git a/deposit_tree/src/eip_4881/merkle_tree.rs b/deposit_tree/src/eip_4881/merkle_tree.rs index f6fb92b9..8cdcd745 100644 --- a/deposit_tree/src/eip_4881/merkle_tree.rs +++ b/deposit_tree/src/eip_4881/merkle_tree.rs @@ -1,14 +1,14 @@ use hashing::{hash_256_256, ZERO_HASHES}; -use typenum::Unsigned; +use typenum::{Unsigned, U32}; use types::phase0::{consts::DepositContractTreeDepth, primitives::DepositIndex}; -use ssz::{Size, SszHash, SszSize, SszWrite, WriteError, H256}; +use ssz::{Size, SszHash, SszRead, SszSize, SszWrite, WriteError, H256}; use lazy_static::lazy_static; use crate::FinalizedDeposit; lazy_static! { static ref ZERO_NODES: Vec = { - (0..=MAX_TREE_DEPTH).map(|n| EIP4881MerkleTree::Zero(n as u32)).collect() + (0..=MAX_TREE_DEPTH).map(|n| EIP4881MerkleTree::Zero(n)).collect() }; } @@ -28,16 +28,101 @@ pub enum EIP4881MerkleTree { } impl SszSize for EIP4881MerkleTree { - const SIZE: Size = Size::for_untagged_union([ - u32::SIZE, - H256::SIZE, - H256::SIZE, - H256::SIZE - ]); + const SIZE: Size = u8::SIZE.add( + Size::for_untagged_union([ + u32::SIZE, + H256::SIZE, + H256::SIZE, + H256::SIZE + ]) + ); } impl SszWrite for EIP4881MerkleTree { + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + let mut length_before = bytes.len(); + + // prefix with code + let code: u8 = match self { + Self::Zero(_) => 0, + Self::Leaf(_) => 1, + Self::Finalized(_) => 2, + Self::Node(_, _, _) => 3 + }; + let size = u8::SIZE.get(); + bytes.resize(length_before + size, 0); + code.write_fixed(&mut bytes[length_before..]); + length_before = bytes.len(); + + match self { + Self::Zero(depth) => { + let size = u32::SIZE.get(); + let length_after = length_before + size; + bytes.resize(length_after, 0); + depth.write_fixed(&mut bytes[length_before..]); + }, + Self::Leaf(hash) | Self::Finalized(hash) => { + let size = H256::SIZE.get(); + let length_after = length_before + size; + bytes.resize(length_after, 0); + hash.write_fixed(&mut bytes[length_before..]); + }, + Self::Node(hash, left, right) => { + let size = H256::SIZE.get(); + let length_after = length_before + size; + bytes.resize(length_after, 0); + hash.write_fixed(&mut bytes[length_before..]); + left.as_ref().write_fixed(bytes); + + // write delimiter between left and right branches + length_before = bytes.len(); + let delimiter: u8 = MAX_TREE_DEPTH as u8 + 1; // it's so not to confuse it with Zero() + bytes.resize(length_before + u32::SIZE.get(), 0); + delimiter.write_fixed(&mut bytes[length_before..]); + + right.as_ref().write_fixed(bytes); + }, + } + + Ok(()) + } +} + +impl SszHash for EIP4881MerkleTree { + fn hash_tree_root(&self) -> H256 { + self.hash() + } + type PackingFactor = U32; +} + +impl SszRead for EIP4881MerkleTree { + fn from_ssz_unchecked(context: &C, bytes: &[u8]) -> Result { + let tree_start = u8::SIZE.get(); + let code_bytes = ssz::subslice(bytes, 0..tree_start)?; + let code = u8::from_ssz(context, code_bytes)?; + let data = ssz::subslice(bytes, tree_start..bytes.len())?; + match code { + 0 => { + let depth = u32::from_ssz(context, data)?; + Ok(Self::Zero(depth)) + }, + 1 => { + let hash = H256::from_ssz(context, data)?; + Ok(Self::Leaf(hash)) + }, + 2 => { + let hash = H256::from_ssz(context, data)?; + Ok(Self::Finalized(hash)) + }, + 3 => { + let hash = H256::from_ssz(context)?; + }, + num if num == (MAX_TREE_DEPTH as u8 + 1) => { + Ok(Zero()) + } + } + } } impl Default for EIP4881MerkleTree { @@ -334,13 +419,13 @@ impl EIP4881MerkleTree { const SPACES: u32 = 10; space += SPACES; let (pair, text) = match self { - Self::Node(hash, left, right) => (Some((left, right)), format!("Node({})", hash)), - Self::Leaf(hash) => (None, format!("Leaf({})", hash)), + Self::Node(hash, left, right) => (Some((left, right)), format!("Node({hash})")), + Self::Leaf(hash) => (None, format!("Leaf({hash})")), Self::Zero(depth) => ( None, format!("Z[{}]({})", depth, ZERO_HASHES[*depth as usize]), ), - Self::Finalized(hash) => (None, format!("Finl({})", hash)), + Self::Finalized(hash) => (None, format!("Finl({hash})")), }; if let Some((_, right)) = pair { right.print_node(space); @@ -349,7 +434,7 @@ impl EIP4881MerkleTree { for _i in SPACES..space { print!(" "); } - println!("{}", text); + println!("{text}"); if let Some((left, _)) = pair { left.print_node(space); } @@ -360,6 +445,7 @@ impl EIP4881MerkleTree { /// /// The `branch` argument is the main component of the proof: it should be a list of internal /// node hashes such that the root can be reconstructed (in bottom-up order). +#[must_use] pub fn verify_merkle_proof( leaf: H256, branch: &[H256], @@ -375,7 +461,13 @@ pub fn verify_merkle_proof( } /// Compute a root hash from a leaf and a Merkle proof. -pub fn merkle_root_from_branch(leaf: H256, branch: &[H256], depth: usize, index: usize) -> H256 { +#[must_use] +pub fn merkle_root_from_branch( + leaf: H256, + branch: &[H256], + depth: usize, + index: usize +) -> H256 { assert_eq!(branch.len(), depth, "proof length should equal depth"); let mut merkle_root = leaf.clone(); @@ -394,7 +486,7 @@ pub fn merkle_root_from_branch(leaf: H256, branch: &[H256], depth: usize, index: impl From for EIP4881MerkleTreeError { fn from(e: InvalidSnapshot) -> Self { - EIP4881MerkleTreeError::InvalidSnapshot(e) + Self::InvalidSnapshot(e) } } From c0041a8e5d74ce41da0de370af39311af94f77a9 Mon Sep 17 00:00:00 2001 From: Sulpiride Date: Sun, 25 Aug 2024 17:06:14 +0500 Subject: [PATCH 5/5] change deposit tree in factory package --- factory/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/src/lib.rs b/factory/src/lib.rs index 8eb54a85..11edd2cb 100644 --- a/factory/src/lib.rs +++ b/factory/src/lib.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use anyhow::{bail, Result}; use bls::AggregateSignature; -use deposit_tree::DepositTree; +use deposit_tree::DepositDataTree; use helper_functions::{ accessors, misc, signing::{RandaoEpoch, SignForSingleFork as _, SignForSingleForkAtSlot as _}, @@ -52,7 +52,7 @@ use types::{ type BlockWithState

= (Arc>, Arc>); -pub fn min_genesis_state(config: &Config) -> Result<(Arc>, DepositTree)> { +pub fn min_genesis_state(config: &Config) -> Result<(Arc>, DepositDataTree)> { let (genesis_state, deposit_tree) = interop::quick_start_beacon_state( config, config.min_genesis_time,