From af1354eb928cdac874bec66c990b19b7fa9d57df Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Sun, 2 Aug 2026 17:53:14 +0100 Subject: [PATCH] add initial pbt impl --- Cargo.lock | 13 + Cargo.toml | 3 + crates/common/binary-trie/Cargo.toml | 23 + crates/common/binary-trie/README.md | 53 ++ crates/common/binary-trie/src/embedding.rs | 460 ++++++++++++++++++ crates/common/binary-trie/src/error.rs | 21 + crates/common/binary-trie/src/lib.rs | 13 + .../binary-trie/src/trie/binary_trie.rs | 354 ++++++++++++++ crates/common/binary-trie/src/trie/bits.rs | 68 +++ crates/common/binary-trie/src/trie/mod.rs | 14 + crates/common/binary-trie/src/trie/node.rs | 39 ++ .../common/binary-trie/tests/spec_vectors.rs | 185 +++++++ .../tests/vectors/binary_trie_vectors.json | 390 +++++++++++++++ rust-toolchain.toml | 1 + 14 files changed, 1637 insertions(+) create mode 100644 crates/common/binary-trie/Cargo.toml create mode 100644 crates/common/binary-trie/README.md create mode 100644 crates/common/binary-trie/src/embedding.rs create mode 100644 crates/common/binary-trie/src/error.rs create mode 100644 crates/common/binary-trie/src/lib.rs create mode 100644 crates/common/binary-trie/src/trie/binary_trie.rs create mode 100644 crates/common/binary-trie/src/trie/bits.rs create mode 100644 crates/common/binary-trie/src/trie/mod.rs create mode 100644 crates/common/binary-trie/src/trie/node.rs create mode 100644 crates/common/binary-trie/tests/spec_vectors.rs create mode 100644 crates/common/binary-trie/tests/vectors/binary_trie_vectors.json diff --git a/Cargo.lock b/Cargo.lock index 169802ae282..90490bc60b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3858,6 +3858,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "ethrex-binary-trie" +version = "22.0.0" +dependencies = [ + "blake3", + "ethereum-types 0.15.1", + "hex", + "hex-literal 0.4.1", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "ethrex-blockchain" version = "22.0.0" diff --git a/Cargo.toml b/Cargo.toml index 5d3d7f7f806..bfee2238f51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/common", "crates/common/rlp", "crates/common/trie", + "crates/common/binary-trie", "crates/common/crypto", "crates/l2/", "crates/l2/common", @@ -77,6 +78,7 @@ ethrex-storage = { path = "./crates/storage", version = "22.0.0" } ethrex-vm = { path = "./crates/vm", version = "22.0.0", default-features = false } ethrex-levm = { path = "./crates/vm/levm", version = "22.0.0" } ethrex-trie = { path = "./crates/common/trie", version = "22.0.0" } +ethrex-binary-trie = { path = "./crates/common/binary-trie", version = "22.0.0" } ethrex-rlp = { path = "./crates/common/rlp", version = "22.0.0" } ethrex-crypto = { path = "./crates/common/crypto", version = "22.0.0", default-features = false } ethrex-metrics = { path = "./crates/blockchain/metrics", version = "22.0.0" } @@ -108,6 +110,7 @@ hex-literal = "0.4.1" crc32fast = "1.4.2" lazy_static = "1.5.0" lru = "0.16.2" +blake3 = "1" sha2 = "0.10.9" sha3 = "0.10.8" tokio-util = { version = "0.7.15", features = ["rt"] } diff --git a/crates/common/binary-trie/Cargo.toml b/crates/common/binary-trie/Cargo.toml new file mode 100644 index 00000000000..9ffd9572bb0 --- /dev/null +++ b/crates/common/binary-trie/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ethrex-binary-trie" +version.workspace = true +edition.workspace = true +authors.workspace = true +documentation.workspace = true +license.workspace = true +description = "EIP-8297 Partitioned Binary Tree for the ethrex Ethereum execution client" +repository.workspace = true + +[dependencies] +ethereum-types.workspace = true +blake3.workspace = true +thiserror.workspace = true + +[dev-dependencies] +hex.workspace = true +serde.workspace = true +serde_json.workspace = true +hex-literal.workspace = true + +[lints] +workspace = true diff --git a/crates/common/binary-trie/README.md b/crates/common/binary-trie/README.md new file mode 100644 index 00000000000..407b4790ebe --- /dev/null +++ b/crates/common/binary-trie/README.md @@ -0,0 +1,53 @@ +## Ethrex-Binary-Trie + +Implementation of the [EIP-8297](https://eips.ethereum.org/EIPS/eip-8297) +Partitioned Binary Tree: the binary trie proposed to replace the Merkle +Patricia Trie as Ethereum's state commitment. + +The crate has two halves: + +- `trie` — the raw tree: a compressed binary radix trie mapping + prefix-free variable-length keys to 32-byte values, committing to its + contents with BLAKE3 over tagged leaf/branch preimages + (`blake3(0x00 ‖ key ‖ value)`, `blake3(0x01 ‖ prefix ‖ left ‖ right)`) + up to a single root. The empty tree's root is the 32-zero-byte + sentinel `EMPTY_TRIE_ROOT`. +- `embedding` — the state embedding: how accounts, storage and code map + onto tree keys and values. Zone-prefixed keys, per-account header + stems (basic data, code hash, first 64 storage slots, first 128 code + chunks under one stem), overflow storage/code key derivation + (`get_tree_key_for_storage_slot`, `get_tree_key_for_code_chunk`), + code chunking (`chunkify_code`) and basic-data packing + (`encode_basic_data`). + +### The trie + +`trie::BinaryTrie` is incremental and insertion-based. Its canonical +structure is insertion-order independent: any insertion order over the +same key/value set yields the same root. Correctness is pinned by the +spec conformance vectors rather than by a second in-crate +implementation. + +### Test vectors + +`tests/vectors/binary_trie_vectors.json` is **vendored** from the EELS +reference implementation, which owns the generator and its schema +documentation: + + ethereum/execution-specs, branch projects/binary-trie + tests/binary_trie/vectors/ + +**Vendored from:** `projects/binary-trie` @ `f8986dca` + +### Non-goals (today) + +No persistence/`TrieDB` backing, no hash caching, no deletion, no +proofs, no fork wiring. These are deferred to the state-commitment +integration work. + +### Spec discrepancy + +`encode_basic_data` packs `code_size` as 4 bytes at offset 4, following +the EELS branch this crate is ported from; EIP-7864 specifies a 3-byte +field at offset 5. The conformance fixture pins the EELS choice — +revisit when EIP-8297's final layout lands. diff --git a/crates/common/binary-trie/src/embedding.rs b/crates/common/binary-trie/src/embedding.rs new file mode 100644 index 00000000000..7ee2f96edc8 --- /dev/null +++ b/crates/common/binary-trie/src/embedding.rs @@ -0,0 +1,460 @@ +//! EIP-8297 Ethereum state embedding: maps accounts, storage slots and +//! contract code onto binary-tree keys and 32-byte leaf values. +//! +//! Account and storage tries are merged into the single key/value tree +//! implemented in [`crate::trie`], which also holds contract code. +//! +//! The first byte of every key is a **zone** identifier labeling the +//! category of state the key holds: account headers live in +//! [`ACCOUNT_ZONE`], content-addressed overflow code in [`CODE_ZONE`], +//! and overflow storage in [`STORAGE_ZONE`]. Keys are variable length, +//! but every key of a zone has the same length, keeping keys +//! prefix-free as the tree requires. +//! +//! A key's **stem** is every byte except its final sub-index byte. +//! Keys sharing a stem form one group of up to [`STEM_SUBTREE_WIDTH`] +//! co-located values, all reachable through the same branch of the +//! tree. This keeps data that is accessed together cheap to prove: an +//! account's header stem holds its basic data, code hash, first +//! storage slots, and first code chunks, so one proof path covers +//! them all. + +use ethereum_types::{H160, H256, U256}; + +use crate::error::BinaryTrieError; +use crate::trie::node::blake3_hash; + +/// Sub-index of the account header leaf packing version, code size, +/// nonce, and balance. +pub const BASIC_DATA_LEAF_KEY: u8 = 0; + +/// Version of the basic data leaf layout, packed as the leaf's first +/// byte by [`encode_basic_data`]. A future change to the layout bumps +/// the version so readers can tell the encodings apart. +pub const BASIC_DATA_VERSION: u8 = 0; + +/// Sub-index of the account header leaf holding the code hash. +pub const CODE_HASH_LEAF_KEY: u8 = 1; + +/// Sub-index of storage slot `0` within the account header stem. +/// Slots `0` through `63` live in the header. +pub const HEADER_STORAGE_OFFSET: u64 = 64; + +/// Sub-index of code chunk `0` within the account header stem. +/// Chunks `0` through `127` live in the header. +pub const CODE_OFFSET: u64 = 128; + +/// Maximum number of values grouped under a single stem: the size of +/// the sub-index byte's space. +pub const STEM_SUBTREE_WIDTH: u64 = 256; + +/// Zone byte of account header stems. +pub const ACCOUNT_ZONE: u8 = 0; + +/// Zone byte of content-addressed overflow code stems. +pub const CODE_ZONE: u8 = 1; + +/// Zone byte of overflow storage stems. +/// +/// Storage sits at the far end of the zone byte, leaving zones `2` +/// through `254` reserved for future state categories. +pub const STORAGE_ZONE: u8 = 255; + +/// Length of every account zone key: the zone byte, a full address +/// digest, and the sub-index byte. +pub const ACCOUNT_KEY_LENGTH: usize = 34; + +/// Length of every code zone key: the zone byte, a full digest of the +/// code hash and group index, and the sub-index byte. +pub const CODE_KEY_LENGTH: usize = 34; + +/// Length of every storage zone key: the zone byte, two full digests +/// binding the account and its group index, and the sub-index byte. +pub const STORAGE_KEY_LENGTH: usize = 66; + +/// 32-byte address used to key the tree. Legacy 20-byte addresses are +/// converted by [`address20_to_address32`]. +pub type Address32 = [u8; 32]; + +/// A binary-tree key derived by this embedding. +pub type Key = Vec; + +/// Convert a legacy 20-byte address by prepending 12 zero bytes. +/// +/// The embedding keys the tree by 32-byte addresses so that a future +/// address-space extension needs no re-keying. +pub fn address20_to_address32(address: H160) -> Address32 { + let mut out = [0u8; 32]; + out[12..].copy_from_slice(address.as_bytes()); + out +} + +/// Hash `data` for use in tree key derivation. +/// +/// In practice this reuses the tree's own merkleization hash, +/// [`blake3_hash`]. +fn key_hash(data: &[u8]) -> H256 { + blake3_hash(data) +} + +/// Build a key from its three parts: the `zone` byte, the +/// hash-derived `tree_position`, and the final `sub_index` byte. +fn get_tree_key(zone: u8, tree_position: &[u8], sub_index: u8) -> Key { + let mut key = Vec::with_capacity(2 + tree_position.len()); + key.push(zone); + key.extend_from_slice(tree_position); + key.push(sub_index); + key +} + +/// Compute the key of the account header leaf at `sub_index`. +/// +/// The header stem is in [`ACCOUNT_ZONE`] and is keyed by the address +/// alone, so each account has exactly one header stem. The header is +/// not one key: it is up to [`STEM_SUBTREE_WIDTH`] separate leaves +/// sharing that stem, and `sub_index` selects which one; basic data, +/// code hash, an early storage slot, or an early code chunk. +/// +/// `sub_index` is a `u8` because the sub-index space is exactly the +/// byte's range: every value is in bounds by construction, so callers +/// narrow where they establish the bound rather than relying on a +/// debug-only check here. +pub fn get_tree_key_for_header(address: &Address32, sub_index: u8) -> Key { + let key = get_tree_key(ACCOUNT_ZONE, key_hash(address).as_bytes(), sub_index); + debug_assert_eq!(key.len(), ACCOUNT_KEY_LENGTH); + key +} + +/// Compute the key of the account's basic data leaf. +pub fn get_tree_key_for_basic_data(address: &Address32) -> Key { + get_tree_key_for_header(address, BASIC_DATA_LEAF_KEY) +} + +/// Compute the key of the account's code hash leaf. +pub fn get_tree_key_for_code_hash(address: &Address32) -> Key { + get_tree_key_for_header(address, CODE_HASH_LEAF_KEY) +} + +/// Build the hash-derived position of an account's overflow storage +/// group at `tree_index`. +/// +/// The position carries two full digests: +/// +/// - `key_hash(address)` gathers all of an account's overflow storage +/// under one subtree, which future expiry and sync schemes could use +/// as their unit of work: a contract's whole storage is one +/// contiguous key range rather than locations scattered across the +/// whole tree. +/// - `key_hash(address ‖ tree_index)` spreads the account's groups +/// within that subtree. +/// +/// Both digests depend on the address, so storage keys that an +/// attacker grinds to sit close together under one contract cannot be +/// reused against a different contract. +fn storage_tree_position(address: &Address32, tree_index: U256) -> Vec { + let prefix = key_hash(address); + let mut preimage = Vec::with_capacity(64); + preimage.extend_from_slice(address); + preimage.extend_from_slice(&tree_index.to_big_endian()); + let suffix = key_hash(&preimage); + let mut position = Vec::with_capacity(64); + position.extend_from_slice(prefix.as_bytes()); + position.extend_from_slice(suffix.as_bytes()); + position +} + +/// Compute the key of a storage slot. +/// +/// Slots `0` through `63` live in the account header stem at +/// sub-indices [`HEADER_STORAGE_OFFSET`] onward; all other slots live +/// in [`STORAGE_ZONE`], grouped [`STEM_SUBTREE_WIDTH`] consecutive +/// slots to a stem. This leaves group `0` (`tree_index == 0`) short: +/// its storage-zone leaves are only sub-indices `64`-`255`. +pub fn get_tree_key_for_storage_slot(address: &Address32, storage_key: U256) -> Key { + if storage_key < U256::from(CODE_OFFSET - HEADER_STORAGE_OFFSET) { + // `low_u64` cannot truncate: the slot is below 64. + return get_tree_key_for_header( + address, + (HEADER_STORAGE_OFFSET + storage_key.low_u64()) as u8, + ); + } + let width = U256::from(STEM_SUBTREE_WIDTH); + let tree_index = storage_key / width; + // `low_u64` cannot truncate: the remainder is below 256. + let sub_index = (storage_key % width).low_u64() as u8; + let key = get_tree_key( + STORAGE_ZONE, + &storage_tree_position(address, tree_index), + sub_index, + ); + debug_assert_eq!(key.len(), STORAGE_KEY_LENGTH); + key +} + +/// Compute the key of a code chunk. +/// +/// Chunks `0` through `127` live in the account header stem: the start +/// of a contract's code (usually dispatchers and entry points) is its +/// most executed region, so the first chunks open with the same branch +/// as the account's basic data. +/// +/// Chunks at index `128` and above live in [`CODE_ZONE`], +/// content-addressed by `code_hash` so contracts with identical +/// bytecode share leaves. +pub fn get_tree_key_for_code_chunk( + address: &Address32, + code_hash: &[u8; 32], + chunk_id: u64, +) -> Key { + let header_chunk_count = STEM_SUBTREE_WIDTH - CODE_OFFSET; + if chunk_id < header_chunk_count { + return get_tree_key_for_header(address, (CODE_OFFSET + chunk_id) as u8); + } + let overflow = chunk_id - header_chunk_count; + let tree_index = overflow / STEM_SUBTREE_WIDTH; + let sub_index = (overflow % STEM_SUBTREE_WIDTH) as u8; + let mut preimage = Vec::with_capacity(64); + preimage.extend_from_slice(code_hash); + preimage.extend_from_slice(&U256::from(tree_index).to_big_endian()); + let key = get_tree_key(CODE_ZONE, key_hash(&preimage).as_bytes(), sub_index); + debug_assert_eq!(key.len(), CODE_KEY_LENGTH); + key +} + +/// Opcode value one below `PUSH1`, so `PUSH_OFFSET + n` is the opcode +/// pushing `n` bytes. +pub const PUSH_OFFSET: u8 = 95; + +/// Opcode of the smallest push instruction. +pub const PUSH1: u8 = PUSH_OFFSET + 1; + +/// Opcode of the largest push instruction. +pub const PUSH32: u8 = PUSH_OFFSET + 32; + +/// Split `code` into the 32-byte chunks stored in the tree. +/// +/// Chunk `i` holds the `i`-th 31-byte slice of the code (zero-padded) +/// in bytes `1` through `31`, preceded by one byte counting how many +/// of the slice's leading bytes are data of a push instruction that +/// began in an earlier chunk. The count lets a chunk be interpreted +/// without its predecessors and is capped at `31`, the chunk payload +/// size. +pub fn chunkify_code(code: &[u8]) -> Vec<[u8; 32]> { + let padded_len = code.len().div_ceil(31) * 31; + let mut code = code.to_vec(); + code.resize(padded_len, 0); + + // Number of push-data bytes remaining at each position, counting + // the position itself; `0` marks executable bytes. The extra 32 + // entries let the largest push record data past the end of the + // code. + let mut remaining_push_data = vec![0u8; padded_len + 32]; + let mut position = 0; + while position < padded_len { + let opcode = code[position]; + let push_data_bytes = if (PUSH1..=PUSH32).contains(&opcode) { + (opcode - PUSH_OFFSET) as usize + } else { + 0 + }; + position += 1; + for offset in 0..push_data_bytes { + remaining_push_data[position + offset] = (push_data_bytes - offset) as u8; + } + position += push_data_bytes; + } + + (0..padded_len) + .step_by(31) + .map(|start| { + let mut chunk = [0u8; 32]; + chunk[0] = remaining_push_data[start].min(31); + chunk[1..].copy_from_slice(&code[start..start + 31]); + chunk + }) + .collect() +} + +/// Pack an account's basic data into the 32-byte value stored at +/// [`BASIC_DATA_LEAF_KEY`]. +/// +/// The fields are packed big-endian: one version byte, three reserved +/// zero bytes, four bytes of code size, eight bytes of nonce, and +/// sixteen bytes of balance. Balances are protocol-level `U256` +/// values, so the sixteen-byte field bound is checked here and +/// [`BinaryTrieError::BalanceTooLarge`] returned past it. +/// +/// Note: the 4-byte code size at offset 4 follows the EELS branch +/// this crate is ported from, which differs from EIP-7864's 3-byte +/// field at offset 5; the conformance fixture pins this choice. +pub fn encode_basic_data( + code_size: u32, + nonce: u64, + balance: U256, +) -> Result<[u8; 32], BinaryTrieError> { + if balance >= U256::from(1) << 128 { + return Err(BinaryTrieError::BalanceTooLarge); + } + let mut out = [0u8; 32]; + out[0] = BASIC_DATA_VERSION; + // Bytes 1..4 are reserved zeros: headroom for future header fields. + out[4..8].copy_from_slice(&code_size.to_be_bytes()); + out[8..16].copy_from_slice(&nonce.to_be_bytes()); + out[16..32].copy_from_slice(&balance.to_big_endian()[16..]); + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use ethereum_types::{H160, U256}; + use hex_literal::hex; + + const ADDR20: H160 = H160(hex!("00112233445566778899aabbccddeeff00112233")); + + #[test] + fn address32_prepends_twelve_zero_bytes() { + let a32 = address20_to_address32(ADDR20); + assert_eq!(&a32[..12], &[0u8; 12]); + assert_eq!(&a32[12..], ADDR20.as_bytes()); + } + + #[test] + fn basic_data_key_vector() { + // fixture: embedding.basic_data_key + assert_eq!( + get_tree_key_for_basic_data(&address20_to_address32(ADDR20)), + hex!("00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0800").to_vec() + ); + } + + #[test] + fn header_key_layout() { + let a32 = address20_to_address32(ADDR20); + let key = get_tree_key_for_header(&a32, 255); + assert_eq!(key.len(), ACCOUNT_KEY_LENGTH); + assert_eq!(key[0], ACCOUNT_ZONE); + assert_eq!(key[33], 255); + assert_eq!(get_tree_key_for_code_hash(&a32)[..33], key[..33]); + assert_eq!(get_tree_key_for_code_hash(&a32)[33], 1); + } + + #[test] + fn storage_slot_63_in_header_64_in_storage_zone() { + let a32 = address20_to_address32(ADDR20); + let slot63 = get_tree_key_for_storage_slot(&a32, U256::from(63)); + let slot64 = get_tree_key_for_storage_slot(&a32, U256::from(64)); + assert_eq!(slot63.len(), ACCOUNT_KEY_LENGTH); + assert_eq!(slot63[0], ACCOUNT_ZONE); + assert_eq!(slot63[33], 64 + 63); + assert_eq!(slot64.len(), STORAGE_KEY_LENGTH); + assert_eq!(slot64[0], STORAGE_ZONE); + assert_eq!(slot64[65], 64); + } + + #[test] + fn storage_slot_group_zero_is_short() { + let a32 = address20_to_address32(ADDR20); + let k255 = get_tree_key_for_storage_slot(&a32, U256::from(255)); + let k256 = get_tree_key_for_storage_slot(&a32, U256::from(256)); + assert_eq!( + k255[..65], + get_tree_key_for_storage_slot(&a32, U256::from(64))[..65] + ); + assert_ne!(k255[..65], k256[..65]); + } + + #[test] + fn huge_storage_key_does_not_overflow() { + let a32 = address20_to_address32(ADDR20); + let key = get_tree_key_for_storage_slot(&a32, U256::from(2).pow(U256::from(200))); + assert_eq!(key.len(), STORAGE_KEY_LENGTH); + assert_eq!(key[0], STORAGE_ZONE); + } + + #[test] + fn code_chunk_127_in_header_128_in_code_zone() { + let a32 = address20_to_address32(ADDR20); + let code_hash = [0x11u8; 32]; + let c127 = get_tree_key_for_code_chunk(&a32, &code_hash, 127); + let c128 = get_tree_key_for_code_chunk(&a32, &code_hash, 128); + assert_eq!(c127[0], ACCOUNT_ZONE); + assert_eq!(c127[33], 128 + 127); + assert_eq!(c128[0], CODE_ZONE); + assert_eq!(c128.len(), CODE_KEY_LENGTH); + assert_eq!(c128[33], 0); + } + + #[test] + fn chunkify_empty_code_is_empty() { + assert!(chunkify_code(&[]).is_empty()); + } + + #[test] + fn chunkify_pads_to_31_and_prepends_offset_byte() { + let chunks = chunkify_code(&[0x00]); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0][0], 0); + assert_eq!(chunks[0][1], 0x00); + assert_eq!(&chunks[0][2..], &[0u8; 30]); + } + + #[test] + fn chunkify_push4_spilling_into_next_chunk() { + // PUSH4 at position 29; data at 30..34; chunk 1 starts at 31 + // with 3 leading push-data bytes. + let mut code = vec![0x01; 29]; + code.push(0x63); + code.extend([0xaa, 0xbb, 0xcc, 0xdd]); + code.extend([0x01; 10]); + let chunks = chunkify_code(&code); + assert_eq!(chunks.len(), 2); + assert_eq!(chunks[0][0], 0); + assert_eq!(chunks[1][0], 3); + } + + #[test] + fn chunkify_caps_offset_byte_at_31() { + // PUSH32 as last byte of chunk 0: all 32 data bytes follow, but + // the count byte saturates at 31. + let mut code = vec![0x01; 30]; + code.push(0x7f); + code.extend(0..32u8); + code.extend([0x01; 5]); + let chunks = chunkify_code(&code); + assert_eq!(chunks[1][0], 31); + } + + #[test] + fn encode_basic_data_layout_vector() { + // fixture: encode_basic_data[1] + assert_eq!( + encode_basic_data(1234, 42, U256::from(10).pow(U256::from(18))).unwrap(), + hex!("00000000000004d2000000000000002a00000000000000000de0b6b3a7640000") + ); + } + + #[test] + fn encode_basic_data_rejects_balance_at_2_pow_128() { + assert_eq!( + encode_basic_data(0, 0, U256::from(1) << 128), + Err(BinaryTrieError::BalanceTooLarge) + ); + assert!(encode_basic_data(0, 0, (U256::from(1) << 128) - 1).is_ok()); + } + + #[test] + fn overflow_code_is_content_addressed_not_per_account() { + let a = address20_to_address32(ADDR20); + let b = address20_to_address32(H160([0x99; 20])); + let code_hash = [0x11u8; 32]; + assert_eq!( + get_tree_key_for_code_chunk(&a, &code_hash, 128), + get_tree_key_for_code_chunk(&b, &code_hash, 128) + ); + assert_ne!( + get_tree_key_for_code_chunk(&a, &code_hash, 0), + get_tree_key_for_code_chunk(&b, &code_hash, 0) + ); + } +} diff --git a/crates/common/binary-trie/src/error.rs b/crates/common/binary-trie/src/error.rs new file mode 100644 index 00000000000..5d132689a9c --- /dev/null +++ b/crates/common/binary-trie/src/error.rs @@ -0,0 +1,21 @@ +use thiserror::Error; + +use crate::trie::MAX_KEY_LENGTH; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum BinaryTrieError { + /// The empty key is a prefix of every other key. + #[error("empty key")] + EmptyKey, + /// Key exceeds [`MAX_KEY_LENGTH`], past which a branch prefix bit + /// count could overflow its two-byte encoding. + #[error("key longer than {MAX_KEY_LENGTH} bytes")] + KeyTooLong, + /// Inserting this key would make some key a prefix of another, + /// which the tree cannot represent (a leaf terminates its path). + #[error("key is a prefix of another key in the trie")] + PrefixViolation, + /// Balance does not fit the 16-byte field of the basic data leaf. + #[error("balance does not fit the 16-byte basic-data field")] + BalanceTooLarge, +} diff --git a/crates/common/binary-trie/src/lib.rs b/crates/common/binary-trie/src/lib.rs new file mode 100644 index 00000000000..aedf543b28c --- /dev/null +++ b/crates/common/binary-trie/src/lib.rs @@ -0,0 +1,13 @@ +//! EIP-8297 Partitioned Binary Tree. +//! +//! [`trie`] is the raw prefix-free key/value tree committing to its +//! contents with a single BLAKE3 root. [`embedding`] maps Ethereum +//! state (accounts, storage, code) onto tree keys and values. +//! +//! Reference: `ethereum/execution-specs`, `src/ethereum/binary_trie/`. + +pub mod embedding; +pub mod error; +pub mod trie; + +pub use error::BinaryTrieError; diff --git a/crates/common/binary-trie/src/trie/binary_trie.rs b/crates/common/binary-trie/src/trie/binary_trie.rs new file mode 100644 index 00000000000..591ef0ff2a3 --- /dev/null +++ b/crates/common/binary-trie/src/trie/binary_trie.rs @@ -0,0 +1,354 @@ +//! Incremental insertion-based binary radix trie. +//! +//! The trie retains its node structure across insertions, splitting +//! nodes on descent, and hashes that structure on [`BinaryTrie::root`]. +//! Canonical-form invariant: a branch's prefix is exactly the bits its +//! two subtrees share beyond the parent split, so the structure — and +//! therefore the root — depends only on the key/value set, never on +//! insertion order. +//! +//! Deliberate simplifications, deferred to the storage-integration +//! plan: no hash caching (every `root()` call rehashes the whole +//! tree), no deletion, and no `TrieDB` backing (all nodes live in +//! memory). Recursion depth is bounded by the key length in bits, so +//! max-length keys could theoretically overflow the stack; embedding +//! keys are at most 66 bytes, and the storage-integration rework is +//! the place to go iterative if ever needed. + +use ethereum_types::H256; + +use crate::error::BinaryTrieError; + +use super::MAX_KEY_LENGTH; +use super::bits::bytes_to_bits; +use super::node::{EMPTY_TRIE_ROOT, branch_hash, leaf_hash}; + +enum Node { + Leaf { + key: Vec, + value: [u8; 32], + }, + Branch { + /// Bits (0/1 per element) shared by every key below, relative + /// to the parent's split point. + prefix: Vec, + left: Box, + right: Box, + }, +} + +/// Compressed binary radix trie over prefix-free byte keys and +/// 32-byte values, committing to its contents with a BLAKE3 root. +#[derive(Default)] +pub struct BinaryTrie { + root: Option, +} + +impl BinaryTrie { + pub fn new() -> Self { + Self::default() + } + + /// Insert `key` with `value`, overwriting any existing value for + /// the same key. + /// + /// # Errors + /// + /// The trie is left unchanged on every error: + /// - [`BinaryTrieError::EmptyKey`] if `key` is empty. + /// - [`BinaryTrieError::KeyTooLong`] if `key` exceeds + /// [`MAX_KEY_LENGTH`] bytes. + /// - [`BinaryTrieError::PrefixViolation`] if inserting `key` would + /// make some key a bit-prefix of another. + pub fn insert(&mut self, key: Vec, value: [u8; 32]) -> Result<(), BinaryTrieError> { + if key.is_empty() { + return Err(BinaryTrieError::EmptyKey); + } + if key.len() > MAX_KEY_LENGTH { + return Err(BinaryTrieError::KeyTooLong); + } + let bits = bytes_to_bits(&key); + match &mut self.root { + None => { + self.root = Some(Node::Leaf { key, value }); + Ok(()) + } + Some(node) => Self::insert_at(node, &bits, 0, key, value), + } + } + + /// Length of the run where `bits` from `depth` agrees with `other`. + /// + /// Errors when `bits` runs out first: the new key would then be a + /// bit-prefix of what already lives below, which the tree cannot + /// represent. + fn shared_len(bits: &[u8], depth: usize, other: &[u8]) -> Result { + for (i, expected) in other.iter().enumerate() { + match bits.get(depth + i) { + None => return Err(BinaryTrieError::PrefixViolation), + Some(bit) if bit != expected => return Ok(i), + Some(_) => {} + } + } + Ok(other.len()) + } + + /// Insert into the subtree rooted at `node`, whose path from the + /// trie root has consumed the first `depth` bits of every key below + /// it. + /// + /// Every failure is detected before anything is mutated, so an + /// error leaves the subtree — and therefore the trie — untouched. + fn insert_at( + node: &mut Node, + bits: &[u8], + depth: usize, + key: Vec, + value: [u8; 32], + ) -> Result<(), BinaryTrieError> { + // Read-only phase: find the bit position to split at, or return + // having changed nothing. + let split = match node { + Node::Leaf { + key: leaf_key, + value: leaf_value, + } => { + if *leaf_key == key { + *leaf_value = value; + return Ok(()); + } + // A leaf reached at `depth` consumed that many of its + // own bits, so slicing from `depth` is always in range. + let other = bytes_to_bits(leaf_key); + let shared = Self::shared_len(bits, depth, &other[depth..])?; + if depth + shared >= other.len() { + // The stored key ran out first: it is a prefix of + // the new one. + return Err(BinaryTrieError::PrefixViolation); + } + depth + shared + } + Node::Branch { + prefix, + left, + right, + } => { + let shared = Self::shared_len(bits, depth, prefix)?; + if shared == prefix.len() { + // Full prefix match: descend on the bit at the split. + let split = depth + prefix.len(); + let Some(&bit) = bits.get(split) else { + return Err(BinaryTrieError::PrefixViolation); + }; + let child = if bit == 0 { left } else { right }; + return Self::insert_at(child, bits, split + 1, key, value); + } + depth + shared + } + }; + + // Mutating phase: split `node` in two, with the leaf and the + // displaced subtree ordered by the key's bit at `split`. + let displaced = std::mem::replace( + node, + Node::Leaf { + key: Vec::new(), + value: [0; 32], + }, + ); + let (prefix, displaced) = match displaced { + Node::Leaf { .. } => (bits[depth..split].to_vec(), displaced), + Node::Branch { + mut prefix, + left, + right, + } => { + // The bit at `split` becomes the new branch's split bit, + // so it belongs to neither side's prefix. + let tail = prefix.split_off(split - depth + 1); + prefix.truncate(split - depth); + ( + prefix, + Node::Branch { + prefix: tail, + left, + right, + }, + ) + } + }; + let new_leaf = Box::new(Node::Leaf { key, value }); + let displaced = Box::new(displaced); + let (left, right) = if bits[split] == 0 { + (new_leaf, displaced) + } else { + (displaced, new_leaf) + }; + *node = Node::Branch { + prefix, + left, + right, + }; + Ok(()) + } + + /// Value stored under `key`, or `None` if absent. + pub fn get(&self, key: &[u8]) -> Option<[u8; 32]> { + let bits = bytes_to_bits(key); + let mut node = self.root.as_ref()?; + let mut depth = 0; + loop { + match node { + Node::Leaf { + key: leaf_key, + value, + } => return (leaf_key.as_slice() == key).then_some(*value), + Node::Branch { + prefix, + left, + right, + } => { + let split = depth + prefix.len(); + if split >= bits.len() || bits[depth..split] != prefix[..] { + return None; + } + node = if bits[split] == 0 { left } else { right }; + depth = split + 1; + } + } + } + } + + /// Root hash: [`EMPTY_TRIE_ROOT`] for the empty trie, otherwise + /// the recursive tagged BLAKE3 commitment of the retained node + /// structure. + pub fn root(&self) -> H256 { + match &self.root { + None => EMPTY_TRIE_ROOT, + Some(node) => Self::merkleize(node), + } + } + + fn merkleize(node: &Node) -> H256 { + match node { + Node::Leaf { key, value } => leaf_hash(key, value), + Node::Branch { + prefix, + left, + right, + } => branch_hash(prefix, Self::merkleize(left), Self::merkleize(right)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::error::BinaryTrieError; + use crate::trie::node::EMPTY_TRIE_ROOT; + use hex_literal::hex; + + #[test] + fn empty_trie_root_is_sentinel() { + assert_eq!(BinaryTrie::new().root(), EMPTY_TRIE_ROOT); + } + + #[test] + fn single_leaf_matches_spec_vector() { + let mut trie = BinaryTrie::new(); + trie.insert(vec![0u8; 34], [0x01; 32]).unwrap(); + assert_eq!( + trie.root().0, + hex!("4b60a28dce9f3529d103a26e00fadb98514cbd16ce03b7df752426addef9bbc7") + ); + } + + #[test] + fn get_returns_inserted_value_and_none_for_absent() { + let mut trie = BinaryTrie::new(); + trie.insert(vec![0xab; 34], [7; 32]).unwrap(); + assert_eq!(trie.get(&[0xab; 34]), Some([7; 32])); + assert_eq!(trie.get(&[0xac; 34]), None); + } + + #[test] + fn overwrite_replaces_value() { + let mut trie = BinaryTrie::new(); + trie.insert(vec![0x42; 34], [1; 32]).unwrap(); + trie.insert(vec![0x42; 34], [2; 32]).unwrap(); + assert_eq!(trie.get(&[0x42; 34]), Some([2; 32])); + } + + #[test] + fn rejects_empty_key_and_oversized_key() { + let mut trie = BinaryTrie::new(); + assert_eq!(trie.insert(vec![], [0; 32]), Err(BinaryTrieError::EmptyKey)); + assert_eq!( + trie.insert(vec![0; 8193], [0; 32]), + Err(BinaryTrieError::KeyTooLong) + ); + } + + #[test] + fn rejects_prefix_violations_both_directions() { + let mut trie = BinaryTrie::new(); + trie.insert(vec![0xaa, 0xbb], [1; 32]).unwrap(); + assert_eq!( + trie.insert(vec![0xaa], [2; 32]), + Err(BinaryTrieError::PrefixViolation) + ); + assert_eq!( + trie.insert(vec![0xaa, 0xbb, 0xcc], [2; 32]), + Err(BinaryTrieError::PrefixViolation) + ); + } + + #[test] + fn failed_insert_leaves_trie_unchanged() { + let mut trie = BinaryTrie::new(); + trie.insert(vec![0xaa, 0xbb], [1; 32]).unwrap(); + let root_before = trie.root(); + let _ = trie.insert(vec![0xaa], [2; 32]); + let _ = trie.insert(vec![0xaa, 0xbb, 0xcc], [2; 32]); + assert_eq!(trie.root(), root_before); + assert_eq!(trie.get(&[0xaa, 0xbb]), Some([1; 32])); + } + + #[test] + fn failed_insert_below_branch_leaves_trie_unchanged() { + // Two keys sharing their first 9 bits force a root branch with + // a long prefix, so both Branch-arm error sites are reachable. + let mut trie = BinaryTrie::new(); + trie.insert(vec![0xaa, 0xbb], [1; 32]).unwrap(); + trie.insert(vec![0xaa, 0xcc], [2; 32]).unwrap(); + let root_before = trie.root(); + // Runs out of bits inside the branch's prefix walk. + assert_eq!( + trie.insert(vec![0xaa], [3; 32]), + Err(BinaryTrieError::PrefixViolation) + ); + // Fails at the leaf below the branch, exercising error + // propagation and branch reconstruction on the way back up. + assert_eq!( + trie.insert(vec![0xaa, 0xbb, 0xcc], [3; 32]), + Err(BinaryTrieError::PrefixViolation) + ); + assert_eq!(trie.root(), root_before); + assert_eq!(trie.get(&[0xaa, 0xbb]), Some([1; 32])); + assert_eq!(trie.get(&[0xaa, 0xcc]), Some([2; 32])); + } + + #[test] + fn insertion_order_does_not_change_root() { + let keys: [&[u8]; 3] = [&[0xf0, 0x00], &[0xf1, 0x00], &[0x0f, 0x00]]; + let mut forward = BinaryTrie::new(); + let mut reverse = BinaryTrie::new(); + for k in keys { + forward.insert(k.to_vec(), [9; 32]).unwrap(); + } + for k in keys.iter().rev() { + reverse.insert(k.to_vec(), [9; 32]).unwrap(); + } + assert_eq!(forward.root(), reverse.root()); + } +} diff --git a/crates/common/binary-trie/src/trie/bits.rs b/crates/common/binary-trie/src/trie/bits.rs new file mode 100644 index 00000000000..312bb2838b7 --- /dev/null +++ b/crates/common/binary-trie/src/trie/bits.rs @@ -0,0 +1,68 @@ +//! Bit-level helpers. Bits are `Vec` of 0/1 values, MSB-first, +//! matching the spec's readability-first representation. + +/// Expand each byte into eight bits, most significant bit first. +/// +/// Sized up front rather than collected from an iterator: `flat_map` +/// reports no size hint, so `collect` would grow the vector through +/// seven or eight reallocations and land at roughly twice the needed +/// capacity. +pub fn bytes_to_bits(data: &[u8]) -> Vec { + let mut bits = vec![0u8; data.len() * 8]; + for (byte, chunk) in data.iter().zip(bits.chunks_exact_mut(8)) { + for (offset, bit) in chunk.iter_mut().enumerate() { + *bit = (byte >> (7 - offset)) & 1; + } + } + bits +} + +/// Encode a branch prefix: a two-byte big-endian bit count followed by +/// the bits packed MSB-first, zero-padded to a byte boundary. +/// +/// The explicit count keeps the encoding injective: without it, two +/// prefixes differing only in trailing zero bits would pack to the +/// same bytes and two different trees could share a root. +pub fn encode_bit_prefix(prefix: &[u8]) -> Vec { + debug_assert!(prefix.iter().all(|b| *b <= 1), "prefix bits must be 0 or 1"); + assert!( + prefix.len() < 1 << 16, + "prefix bit count must fit in two bytes" + ); + let mut out = vec![0u8; 2 + prefix.len().div_ceil(8)]; + out[..2].copy_from_slice(&(prefix.len() as u16).to_be_bytes()); + for (i, bit) in prefix.iter().enumerate() { + out[2 + i / 8] |= bit << (7 - i % 8); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bytes_to_bits_msb_first() { + assert_eq!(bytes_to_bits(&[0b1010_0001]), vec![1, 0, 1, 0, 0, 0, 0, 1]); + assert_eq!(bytes_to_bits(&[]), Vec::::new()); + assert_eq!(bytes_to_bits(&[0x80, 0x01])[0], 1); + assert_eq!(bytes_to_bits(&[0x80, 0x01])[15], 1); + } + + #[test] + fn encode_bit_prefix_empty() { + assert_eq!(encode_bit_prefix(&[]), vec![0x00, 0x00]); + } + + #[test] + fn encode_bit_prefix_packs_msb_first_and_pads() { + assert_eq!(encode_bit_prefix(&[1, 0, 1]), vec![0x00, 0x03, 0b1010_0000]); + let nine = vec![1, 1, 1, 1, 1, 1, 1, 1, 1]; + assert_eq!(encode_bit_prefix(&nine), vec![0x00, 0x09, 0xff, 0x80]); + } + + #[test] + fn encode_bit_prefix_is_injective_on_trailing_zeros() { + assert_ne!(encode_bit_prefix(&[1]), encode_bit_prefix(&[1, 0])); + } +} diff --git a/crates/common/binary-trie/src/trie/mod.rs b/crates/common/binary-trie/src/trie/mod.rs new file mode 100644 index 00000000000..cb8b0b1ded8 --- /dev/null +++ b/crates/common/binary-trie/src/trie/mod.rs @@ -0,0 +1,14 @@ +//! Raw EIP-8297 binary tree: a compressed binary radix trie mapping +//! prefix-free variable-length bit keys to 32-byte values, committing +//! to its contents with BLAKE3 hashes up to a single root. + +mod binary_trie; +mod bits; +pub(crate) mod node; + +pub use binary_trie::BinaryTrie; +pub use node::EMPTY_TRIE_ROOT; + +/// Longest accepted key, in bytes. Bounds branch-prefix bit counts +/// below the two-byte limit of `encode_bit_prefix`. +pub const MAX_KEY_LENGTH: usize = 8192; diff --git a/crates/common/binary-trie/src/trie/node.rs b/crates/common/binary-trie/src/trie/node.rs new file mode 100644 index 00000000000..a61c28984b5 --- /dev/null +++ b/crates/common/binary-trie/src/trie/node.rs @@ -0,0 +1,39 @@ +//! Node hashing: the leaf and branch preimages that commit the +//! tree's contents to a single BLAKE3 root. + +use ethereum_types::H256; + +use super::bits::encode_bit_prefix; + +/// Root hash of an empty tree: a 32-zero-byte sentinel, not a hash output. +pub const EMPTY_TRIE_ROOT: H256 = H256([0u8; 32]); + +const LEAF_NODE_TAG: u8 = 0x00; +const BRANCH_NODE_TAG: u8 = 0x01; + +pub(crate) fn blake3_hash(data: &[u8]) -> H256 { + H256(*blake3::hash(data).as_bytes()) +} + +/// Hash committing to a leaf: `blake3(0x00 ‖ full_key ‖ value)`. +/// The complete key is committed so a leaf's meaning never depends +/// on the path taken to reach it. +pub(super) fn leaf_hash(key: &[u8], value: &[u8; 32]) -> H256 { + let mut preimage = Vec::with_capacity(1 + key.len() + 32); + preimage.push(LEAF_NODE_TAG); + preimage.extend_from_slice(key); + preimage.extend_from_slice(value); + blake3_hash(&preimage) +} + +/// Hash committing to a branch: +/// `blake3(0x01 ‖ encode_bit_prefix(prefix) ‖ left ‖ right)`. +pub(super) fn branch_hash(prefix: &[u8], left: H256, right: H256) -> H256 { + let encoded_prefix = encode_bit_prefix(prefix); + let mut preimage = Vec::with_capacity(1 + encoded_prefix.len() + 64); + preimage.push(BRANCH_NODE_TAG); + preimage.extend_from_slice(&encoded_prefix); + preimage.extend_from_slice(left.as_bytes()); + preimage.extend_from_slice(right.as_bytes()); + blake3_hash(&preimage) +} diff --git a/crates/common/binary-trie/tests/spec_vectors.rs b/crates/common/binary-trie/tests/spec_vectors.rs new file mode 100644 index 00000000000..371dcee2a6d --- /dev/null +++ b/crates/common/binary-trie/tests/spec_vectors.rs @@ -0,0 +1,185 @@ +//! Conformance tests against vectors generated from the EELS +//! reference implementation. The fixture is vendored from +//! execution-specs `tests/binary_trie/vectors/`, which owns the +//! generator; see this crate's README for how to refresh it. + +use std::collections::BTreeMap; + +use ethereum_types::{H160, U256}; +use ethrex_binary_trie::embedding; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Fixture { + trie_roots: Vec, + embedding: EmbeddingVectors, + chunkify_code: Vec, + encode_basic_data: Vec, +} + +#[derive(Deserialize)] +struct ChunkifyCase { + name: String, + code: String, + chunks: Vec, +} + +#[derive(Deserialize)] +struct BasicDataCase { + code_size: u32, + nonce: u64, + /// Hex string; balances can exceed `u64`. + balance: String, + encoded: String, +} + +#[derive(Deserialize)] +struct EmbeddingVectors { + address20: String, + address32: String, + basic_data_key: String, + code_hash_key: String, + header_sub_index_255_key: String, + /// Keyed by decimal slot number, including values past `u64`. + storage_slot_keys: BTreeMap, + /// Keyed by decimal chunk id. + code_chunk_keys: BTreeMap, + code_hash: String, +} + +#[derive(Deserialize)] +struct TrieCase { + name: String, + entries: Vec, + root: String, +} + +#[derive(Deserialize)] +struct Entry { + key: String, + value: String, +} + +fn unhex(s: &str) -> Vec { + hex::decode(s.strip_prefix("0x").unwrap_or(s)).expect("fixture hex string") +} + +fn load() -> Fixture { + let fixture: Fixture = + serde_json::from_str(include_str!("vectors/binary_trie_vectors.json")).unwrap(); + // The fixture is vendored and refreshed from upstream, so its case + // counts are expected to grow. Assert only that no section arrived + // empty — an exact count would fail every legitimate refresh. + assert!(!fixture.trie_roots.is_empty(), "no trie root cases"); + assert!(!fixture.chunkify_code.is_empty(), "no chunkify cases"); + assert!(!fixture.encode_basic_data.is_empty(), "no basic-data cases"); + assert!( + !fixture.embedding.storage_slot_keys.is_empty(), + "no storage slot keys" + ); + assert!( + !fixture.embedding.code_chunk_keys.is_empty(), + "no code chunk keys" + ); + fixture +} + +#[test] +fn embedding_keys_match_spec() { + let vectors = load().embedding; + + let address20 = H160::from_slice(&unhex(&vectors.address20)); + let address32 = embedding::address20_to_address32(address20); + assert_eq!(address32.as_slice(), unhex(&vectors.address32).as_slice()); + + assert_eq!( + embedding::get_tree_key_for_basic_data(&address32), + unhex(&vectors.basic_data_key) + ); + assert_eq!( + embedding::get_tree_key_for_code_hash(&address32), + unhex(&vectors.code_hash_key) + ); + assert_eq!( + embedding::get_tree_key_for_header(&address32, 255), + unhex(&vectors.header_sub_index_255_key) + ); + + for (slot, expected) in &vectors.storage_slot_keys { + let storage_key = U256::from_dec_str(slot).expect("fixture decimal slot"); + assert_eq!( + embedding::get_tree_key_for_storage_slot(&address32, storage_key), + unhex(expected), + "storage slot {slot}" + ); + } + + let code_hash: [u8; 32] = unhex(&vectors.code_hash) + .try_into() + .expect("fixture code hash"); + for (chunk_id, expected) in &vectors.code_chunk_keys { + let chunk_id: u64 = chunk_id.parse().expect("fixture decimal chunk id"); + assert_eq!( + embedding::get_tree_key_for_code_chunk(&address32, &code_hash, chunk_id), + unhex(expected), + "code chunk {chunk_id}" + ); + } +} + +#[test] +fn chunkify_matches_spec() { + let cases = load().chunkify_code; + for case in cases { + let chunks = embedding::chunkify_code(&unhex(&case.code)); + assert_eq!( + chunks.len(), + case.chunks.len(), + "chunkify case {}", + case.name + ); + for (i, (chunk, expected)) in chunks.iter().zip(&case.chunks).enumerate() { + assert_eq!( + chunk.as_slice(), + unhex(expected).as_slice(), + "chunkify case {} chunk {i}", + case.name + ); + } + } +} + +#[test] +fn basic_data_matches_spec() { + let cases = load().encode_basic_data; + for case in cases { + let balance = U256::from_str_radix(case.balance.trim_start_matches("0x"), 16) + .expect("fixture hex balance"); + assert_eq!( + embedding::encode_basic_data(case.code_size, case.nonce, balance) + .unwrap() + .as_slice(), + unhex(&case.encoded).as_slice(), + "basic data case code_size={} nonce={}", + case.code_size, + case.nonce + ); + } +} + +#[test] +fn incremental_matches_spec_roots() { + for case in load().trie_roots { + let mut trie = ethrex_binary_trie::trie::BinaryTrie::new(); + for e in &case.entries { + trie.insert(unhex(&e.key), unhex(&e.value).try_into().unwrap()) + .unwrap(); + } + assert_eq!( + trie.root().as_bytes(), + unhex(&case.root).as_slice(), + "trie case {}", + case.name + ); + } +} diff --git a/crates/common/binary-trie/tests/vectors/binary_trie_vectors.json b/crates/common/binary-trie/tests/vectors/binary_trie_vectors.json new file mode 100644 index 00000000000..3daf039f697 --- /dev/null +++ b/crates/common/binary-trie/tests/vectors/binary_trie_vectors.json @@ -0,0 +1,390 @@ +{ + "source": "ethereum/execution-specs projects/binary-trie", + "source_commit": "f8986dcae69b8951d48ca7ce79b8739ca465a9c4", + "trie_roots": [ + { + "name": "empty", + "entries": [], + "root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "single_leaf", + "entries": [ + { + "key": "0x00000000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + } + ], + "root": "0x4b60a28dce9f3529d103a26e00fadb98514cbd16ce03b7df752426addef9bbc7" + }, + { + "name": "single_leaf_one_byte_key", + "entries": [ + { + "key": "0xab", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + } + ], + "root": "0x2ebeea9f8e2e4bbf6e4ff1b4cf8afbb641d4dfaf9a05469dea3787bbc35188d5" + }, + { + "name": "two_leaves_diverge_first_bit", + "entries": [ + { + "key": "0x00111111111111111111111111111111111111111111111111111111111111111111", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0x80111111111111111111111111111111111111111111111111111111111111111111", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + } + ], + "root": "0x57210f2156bafa91dc33b7528fcfdb50660b902494b80a00347b28949df72816" + }, + { + "name": "two_leaves_diverge_last_bit", + "entries": [ + { + "key": "0x22222222222222222222222222222222222222222222222222222222222222222200", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0x22222222222222222222222222222222222222222222222222222222222222222201", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + } + ], + "root": "0x606cacfcbf218928a25e67e40c8fa9cdf44d15cbc39be41da210cc86db128be9" + }, + { + "name": "three_leaves_shared_prefix", + "entries": [ + { + "key": "0xf0000000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0xf1000000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "key": "0x0f000000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0303030303030303030303030303030303030303030303030303030303030303" + } + ], + "root": "0x50ca5b44506c7aeac67017eef1be8977c69d8d1074b3c870ce9fc6ef0aa18163" + }, + { + "name": "mixed_key_lengths_34_and_66", + "entries": [ + { + "key": "0x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa05", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0xffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb07", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + } + ], + "root": "0x117ecc342fcf3753397737026c26e522b9b23e61cfb4c8aafa0b1c98ca5d507b" + }, + { + "name": "overwrite_takes_last_value", + "entries": [ + { + "key": "0x42424242424242424242424242424242424242424242424242424242424242424242", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0x42424242424242424242424242424242424242424242424242424242424242424242", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + } + ], + "root": "0xe6817b5d8351669a295e51a0ae8459ace26faeb8904b8d9c7e3a00b9f343e0eb" + }, + { + "name": "random_50_keys_seed_8297", + "entries": [ + { + "key": "0x2aa6a8996ce6a78ab232d4ea1c1773f4216f5c6c16e580f784d1a03c7c4069f1b259", + "value": "0x602a3e5c20f394f60ad655f5a52a61487ce7121bc116b2d0036ad7e47298ab30" + }, + { + "key": "0xc44a5069709f82e5cfa1fdb523a09cbf72345d149135921a5ff4c22b590d5a7c6b32", + "value": "0x3fedde8f23715681af22f74b0d34825fb0cd1bb1a530e7e22b99c856937c3878" + }, + { + "key": "0x1d2c29c5a940de63446ffd493abe5469486948d20bcaf06d586dcd3e28507dbe4a4e", + "value": "0x557de7c40c7061a0e096cb67ee0d347c7e35b9d5fe896395ce2c79333c56e171" + }, + { + "key": "0xc62c92521ebf446fb986f84c4f8ea43e59ebf9744a30fccbea4ff32d0d2bd8c41568", + "value": "0x2cabc800545add40c21822901272b48ea6cccfa1574152725f54f504571b5bd3" + }, + { + "key": "0x4f0f6748864445c4f13e4c69d6b258bab15c7dde77da8d296c8c4de46cffb7518235", + "value": "0x11f71843a4e7dcc6fbe826921ed5bf7ff0db9a856f786c5e7662fcb016eb22ae" + }, + { + "key": "0xadbf5828b2759dea65fa9feef2e0580242769f423e60877107826d8de9fe19d6b77f", + "value": "0xecc05f6a68a2a05d955d25c1d2fa2d2c7d9b3948c70e4ba2117150041d162885" + }, + { + "key": "0x2a95680f5f67f0e0049573d9a08353549a058cc5fb50a87b2cadb92edd48f96b70a9", + "value": "0x52afae073e434b41f054a2d42d0783aaf89ab9cd5c1a96ba37d80fc6c222aa2a" + }, + { + "key": "0x7ca6b423f8f439ea5333e201ead1da2c3f3526aa0a4ea90c3467a4cbf3633c14a232", + "value": "0x2ea42ec85d724ce2cae8ff06532b670f94c7d0fffc6651d893d3dfaa605ba04c" + }, + { + "key": "0xd90768dee20dc8e1b17cb000c02d336c5e9115546fcf635a89e02aa72fcb4c35c079", + "value": "0x0ea636b852ecea23cbbf3600bed740a8a944adb3588d8d3f6d6cfe280b1d93a3" + }, + { + "key": "0x876a714a8fc09271610ec4abaf2ecc3ef7504527a6971bbcbd9cc0e0b1399fe933dc", + "value": "0x0fc1f11caa9f7ced770170dcf0c6006c014e5b8598c7e5c268a4203e207eeb9d" + }, + { + "key": "0x76b13611dabe295dc7e8df8402f1d43193a7891998570d7994a579186320cde1a7cf", + "value": "0xd4be299747047c39c7883b32836aed86acd8837f825a22bf260edf37cd998552" + }, + { + "key": "0xb537a077bf364c9e7e61842b3bdb47fc91e28e47954bc8cacb0a5eeeb0d0a32e09c7", + "value": "0x0d6c809686697d9ac4b0a6a4307459b6004e432933a5840e51aea8039deb341e" + }, + { + "key": "0x9581e69689729b644ddc033c7250173bde751225b7aae0bf279121fefe484499f225", + "value": "0xe0b5acd5c7b81bc11b2f923cb23b9725af51184dc9446950d59b3ccabad4336f" + }, + { + "key": "0x13af92c620778540042f70141701f7f364598d6ebb2822006938ca9559dcc9a5b0bd", + "value": "0x65665ad386602970d3d6194340f0e2dd673990c2eed2c11b5f7630cbf1ebef6e" + }, + { + "key": "0x30c89c776af10ea0a5d693cdc56e7cc26caef5a3d5dcc6a5e7f529004ef34608e2bb", + "value": "0xb10acf3de8bb5c127b7e898d2d31468eabf047610137763ef1327d56acf2c361" + }, + { + "key": "0x377ccaab099c7cfa7f22f0e82270e6bcb3e67c35f760c44d3f8d61cce78f37d3f5d6", + "value": "0xea6cd8bbfb9089722a00b8fe12de059493772ebda54edddf1163b16d90d118d8" + }, + { + "key": "0xe5c6b60b227487dabbcca806225c6a0ad42af15e465cb547595a8c185c87806741e1", + "value": "0xe127f236c860d50fc559abe9c734af3fd89099f4d9c3aea506237360ff72cbe3" + }, + { + "key": "0x952500b8e339aa344675e038772785efa67a9e820ff6c94f56f65c63056408e3351a", + "value": "0xa0b3499ec1f8120541ebaea047b0b6eec03967d9621b5136462c034aeb9fe483" + }, + { + "key": "0xedac6f4248259eab137c0d516cc36c9e43e8260444cc6d43200b4fc1a04e715897fc", + "value": "0xbf7049c0cdd86648fdfb8df979de2c5f28ce252ffb440132bad842de3d530aba" + }, + { + "key": "0xe97686bfc1d82e4195c44c37c63822b9a5790f13b85dc7fbad6e98e41ddb8da26fcf", + "value": "0xeed3aadd8b0587817d34d2de3c960c2c97d3f463e71d4b01e1e7672dbc615677" + }, + { + "key": "0x5e49a3cff08549e40d6bfa45cf4751e681323a0a90747af1fbf2a09029d4054c425d", + "value": "0x11ce34b22799c2e56c0fca48683d2befc6a3424a2c8808ed8f80f02f0bf74107" + }, + { + "key": "0xd49a56c989b8250924ace1b95496057c7e79a48be406ff0f033e9867114d5b28eefe", + "value": "0x4221245e55f174c502b407002d59f809841fae554c37b2c28bf5a56307ba4f4b" + }, + { + "key": "0xb8ec5602446a4bf77aa7f76e2d9080611acf14d59e868273574320059a7cce2fc56f", + "value": "0xc0ac6eebfbbb6650e31532413e0e2707a9724cf3b85a45b1e1a524079bfed6f6" + }, + { + "key": "0x63b5fad598b5ac70ac16669d0674b92588710e1f9eb4ecba96ffd3fca62bc81b9fd4", + "value": "0xdc23fe99d39f5d3d5c1fb403a1fa18f537565f634f9750296b07eed92ac295e9" + }, + { + "key": "0x41580a465c0b267026210bdaac106834115822dbf82975cb3187846c0750150b7da8", + "value": "0xc4350d9d1e38ea02d65518957de948ba86b3366e076fd7c2f701b50efa613601" + }, + { + "key": "0x40ee13ffd4d197e1661c65006cbb0dd5728e8236565299eb84063809b26a4e66c23c", + "value": "0xbaf01926523cff84ff29b32d4d19ff4e1fb222ea0187323675b04357c2bd659a" + }, + { + "key": "0xb2d9269956fe58d83f174b8d9fa80d6f7cf195b3e062ae55bd3cdb53d92308b4bd29", + "value": "0x939ed45a7cd62584b0ce4c549609bfdc80de4c33091f5a4d4062d07c09b3573e" + }, + { + "key": "0x5fcfe5231af181ca8711c3e3638ca8f9a6825431131840eccb2e00f8f341eb0e1f33", + "value": "0xe159bb37bea013607a9568c8c5c9c55129fca8aed6f619a2ffc14f29699dcc28" + }, + { + "key": "0x45dc75b619130f903c33151209e131665173d6da28f554a549a5563ad40bd8fa7a40", + "value": "0xeda30ecb83f08dc40f4c8b892b55ea3e8e30e11a67bf20a32c3015668b3797f2" + }, + { + "key": "0x4d2957cf1e0ab2d3bc27c3a7935b3390face7d07c5d73581a6082730c563265a3589", + "value": "0xf6c96dfd3b2ca036f2f025f1103ad5f23e653a5f0964b46355b892f3eab8facd" + }, + { + "key": "0x933625c1c382f8a83e243d584346e3b94e14b0ac5427cb84a0580b4568d2c91aa706", + "value": "0xe33ffa5d02f23d96e83344e80ccc48fca6036c87a4bac368a24ec28a26bb70c3" + }, + { + "key": "0x20455de6ec9ad2b33b187e1738be0e6b41c299ce96940837e49eea316ad1b6b825ee", + "value": "0x82dfb2bcce1e0f9d8b0230c432d05fbb6aaea5dcf13d7ab0709b89b1ceefb745" + }, + { + "key": "0xfda49bac1959069e75394a653a4e44575539f06deafaa084702605e71c00c9b8e81a", + "value": "0x0f626cda6806b58620931dad579dcb887592e8b46f94acf6831b7d3e0bf9076f" + }, + { + "key": "0xef257a2eb664173e42d4ae098eb635434a74c3c37fab969be2e758b1ca0c3a4d390d", + "value": "0x091b81cb0449892c17511cf1529e2f2666af2671b68d155488de18621f0ad1f8" + }, + { + "key": "0xfa3082b0bd39b1fb7011212ae0c3b8b78f6ce9450069eab37c7ffce74ec93d28e949", + "value": "0x300042c9c0d21bab7634484dfa54ae0dd4d579d23107c2c23f805cdcab1f9bc3" + }, + { + "key": "0x0f35b7fd43411a98bd2f77d1d75f9a44b6fd9ae514b9e12a9c94cf5cdefc3460d5d2", + "value": "0xa2d8179579efa9284a8c5d602825290471c426b06f648cdab864615d7ea33e37" + }, + { + "key": "0x5ae984fa36087b9eead78eafd1219a682441ee126a32522e5dd3f1d0a8555e9a3ca4", + "value": "0x2ccb3878ecf6016742eeb5cf1fa88186c65fad7f59e0519510311bc361d4dee4" + }, + { + "key": "0x62fcb1d41412ffc7c74768f39558646479095f648d3fd045ef2d857ea4740752e6e3", + "value": "0x09af31d454b6c1aa915dc28e5ab362396feb89ede60a5d617f14b3de0edc7012" + }, + { + "key": "0xacc4a501bfd10b9d890407484618059b0dd03ed7c759ca881847e201b4d7f326e233", + "value": "0xc5671cb9b0fb74823fd88bf2df0795602f8532b33ea7cb06a90bb6bc4d64013a" + }, + { + "key": "0x5d3305b1d38d84f22ab72f4cff4997f347e91801e2f01337cdae981e9224258a21ce", + "value": "0x8d578546affb0976f291db18f6ebe99e9dc47ec625003f7941861ac52dda72ff" + }, + { + "key": "0x9345d08e09992493c0f9d2f0b1181ed7a123879a02af0bd7c152f6ad75792efa6f96", + "value": "0xcc59af9043525c027d6c392a62f5f2c5d4626f6b70c7c155d5dbfbec73535f1c" + }, + { + "key": "0x334fbdf232ba304810bb66b0d99caf22fc6b9e35ccb1e470688157c5d767029b3add", + "value": "0x2f5470a09aa7e14347be86198b30ea13d6dfba5ebc2d406a8380141ca4993ecd" + }, + { + "key": "0x5f0fb320d16cf97ef07303475f28e0f72b93ae9689be3fe9c50d4bcd2e13e5456fa2", + "value": "0x31c23f0aa04789347e82acd04248dde92bd11674591f24e09d570cc6a782df11" + }, + { + "key": "0x8db965afa0f3685791b7747fac9b55fe6c9dc680654ceb7bd346be02037810e8cb4b", + "value": "0x8a9ed5de1a61fd15b89879d1f6e7d2dfe53238d9bdd9e0af088d05cb1427c966" + }, + { + "key": "0x3cbafb9218233c982e5c84fda1ded1146c2b208160213ea77ed397c125a55310204e", + "value": "0x9e4483e2abc56cbf79b1ea996ec0aacb171ab36f85643bf0f3121a6185964b72" + }, + { + "key": "0x353d69bbc4438bbae4f1006fb980dba7537ecb1614e8d3c123a678720ca9172144dc", + "value": "0x83af19ff2f7f5c89be64c268a9b01837c26e134a170969bad4bcea74f7ee2c1b" + }, + { + "key": "0xcd492f6376b541704fcbd979aaf13147eace07f547497470761e2c980b3fcd46ce44", + "value": "0x94355b1cbd394a0e65112e6eaa08c5293c0b82eb44b89b5eedbb433b74a8e9ed" + }, + { + "key": "0xec2be3bc6b4bdf9944757aa679fefc6f00a3e534ccf1f5045163d94a992d439c20d7", + "value": "0x4f2581fc51143abd11b62ad6de6fd4f3aaf1fe1a2f40a2b882c990c3db4b3c7e" + }, + { + "key": "0x770900a7fbc1f91e3bb2d425f8f65126f69b1cb601962360a44deb58208ab5c18b4d", + "value": "0x53fb7e26d5248117e143ba30c50481564cf4c02b3fa7ad41462423ca237cf851" + }, + { + "key": "0x11d51e7adf1f2771881bb2f625df7b201ab7719e6cbcf565e28b82bb899709d8581a", + "value": "0x9e428672e42d43194d4c9e84e2c9be48b1f0293bc983a847e38ee4743406631c" + } + ], + "root": "0xd966e4d5b3676b62c732a8c267753f375226322ec44b1c1d4f8f8c40de77e9be" + } + ], + "embedding": { + "address20": "0x00112233445566778899aabbccddeeff00112233", + "address32": "0x00000000000000000000000000112233445566778899aabbccddeeff00112233", + "basic_data_key": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0800", + "code_hash_key": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0801", + "header_sub_index_255_key": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a08ff", + "storage_slot_keys": { + "0": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0840", + "1": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0841", + "63": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a087f", + "64": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a08b7b7ba8d57e997347b504830cfb1837de0bb46da8c5c53654442588e0ca0bdbf40", + "255": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a08b7b7ba8d57e997347b504830cfb1837de0bb46da8c5c53654442588e0ca0bdbfff", + "256": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a085b15bdc9241d79c981f6bf4ae56cf1e77d1f9350cf73372e6d792c1c6eb13b3000", + "511": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a085b15bdc9241d79c981f6bf4ae56cf1e77d1f9350cf73372e6d792c1c6eb13b30ff", + "512": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0808fc1122a8c0a65fbdf45fd999b8b9a4f1e09fd74bf5cc02c51d701972a861b000", + "1606938044258990275541962092341162602522202993782792835301376": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a08de40820dcd8994eedc4c374fd005321c147c12928ed5a191b1a0406bd81a0d7000" + }, + "code_chunk_keys": { + "0": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0880", + "1": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0881", + "127": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a08ff", + "128": "0x01348ace48ac0a7316c5dee2e4e5a680ba01413aa18e07403f3c00b4f82f2da55800", + "129": "0x01348ace48ac0a7316c5dee2e4e5a680ba01413aa18e07403f3c00b4f82f2da55801", + "383": "0x01348ace48ac0a7316c5dee2e4e5a680ba01413aa18e07403f3c00b4f82f2da558ff", + "384": "0x01d2482e6e552436a97975ef92aba4baec1ea2f25a2188a2aac744c6d86ac7e3de00" + }, + "code_hash": "0xbcc90f2d6dada5b18e155c17a1c0a55920aae94f39857d39d0d8ed07ae8f228b" + }, + "chunkify_code": [ + { + "name": "empty", + "code": "0x", + "chunks": [] + }, + { + "name": "stop_padded", + "code": "0x00", + "chunks": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + { + "name": "eip_example_push4_boundary", + "code": "0x010101010101010101010101010101010101010101010101010101010163aabbccdd01010101010101010101", + "chunks": [ + "0x00010101010101010101010101010101010101010101010101010101010163aa", + "0x03bbccdd01010101010101010101000000000000000000000000000000000000" + ] + }, + { + "name": "push32_at_chunk_end_spills_31", + "code": "0x0101010101010101010101010101010101010101010101010101010101017f000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0101010101", + "chunks": [ + "0x000101010101010101010101010101010101010101010101010101010101017f", + "0x1f000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e", + "0x011f010101010100000000000000000000000000000000000000000000000000" + ] + } + ], + "encode_basic_data": [ + { + "code_size": 0, + "nonce": 0, + "balance": "0x0", + "encoded": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code_size": 1234, + "nonce": 42, + "balance": "0xde0b6b3a7640000", + "encoded": "0x00000000000004d2000000000000002a00000000000000000de0b6b3a7640000" + }, + { + "code_size": 4294967295, + "nonce": 18446744073709551615, + "balance": "0xffffffffffffffffffffffffffffffff", + "encoded": "0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ] +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 1612370a692..61fbf9ac0af 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] channel = "1.91.0" profile = "default" +components = ["rust-analyzer"] \ No newline at end of file