From 0c49ca8b1ce392aa9ce95273ddae9780f2220e75 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 24 Jun 2025 16:57:16 +1000 Subject: [PATCH 1/3] Type-level struct field access and gindex calc --- tree_hash/src/lib.rs | 1 + tree_hash/src/prototype.rs | 193 +++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 tree_hash/src/prototype.rs diff --git a/tree_hash/src/lib.rs b/tree_hash/src/lib.rs index e5ac66f..20f0ef8 100644 --- a/tree_hash/src/lib.rs +++ b/tree_hash/src/lib.rs @@ -2,6 +2,7 @@ pub mod impls; mod merkle_hasher; mod merkleize_padded; mod merkleize_standard; +pub mod prototype; pub use merkle_hasher::{Error, MerkleHasher}; pub use merkleize_padded::merkleize_padded; diff --git a/tree_hash/src/prototype.rs b/tree_hash/src/prototype.rs new file mode 100644 index 0000000..94e8594 --- /dev/null +++ b/tree_hash/src/prototype.rs @@ -0,0 +1,193 @@ +/* +type Hash256 = [u8; 32]; + +enum Error { + Oops +} + +trait TreeHash { + fn get_field(&self, index: usize) -> Result<&dyn TreeHash, Error>; + + fn merkle_proof(&self, generalized_index: usize) -> Result, Error>; +} +*/ +use std::marker::PhantomData; + +// A path is a sequence of field accesses like `self.foo.bar`. +// +// We can resolve these at compile time, because we're sickos. +struct Path(PhantomData<(First, Rest)>); + +// The field trait is implemented for all struct fields. +// +// It provides conversion from a named field type to a numeric field index. +trait Field { + const NUM_FIELDS: usize; + const INDEX: usize; +} + +// If T implements Resolve it means T has a field named Field of type Output. +trait Resolve { + type Output; + + fn gindex(parent_index: usize) -> usize; +} + +// This impl defines how paths are resolved and converted to gindices. +impl Resolve> for T +where + Self: Resolve, + First: Field, + >::Output: Resolve, +{ + type Output = <>::Output as Resolve>::Output; + + fn gindex(parent_index: usize) -> usize { + // From `get_generalized_index`: + // https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md#ssz-object-to-index + let new_parent_index = >::gindex(parent_index); + >::Output::gindex(new_parent_index) + } +} + +// Some example structs. +struct Nested3 { + x3: Nested2, + y3: Nested1, +} + +struct Nested2 { + x2: Nested1, + y2: Nested1, +} + +struct Nested1 { + x1: u64, + y1: Vec, +} + +// Fields of Nested3 (these would be generated). +struct FieldX3; +struct FieldY3; + +impl Field for FieldX3 { + const NUM_FIELDS: usize = 2; + const INDEX: usize = 0; +} + +impl Field for FieldY3 { + const NUM_FIELDS: usize = 2; + const INDEX: usize = 1; +} + +// Fields of Nested2 (generated). +struct FieldX2; +struct FieldY2; + +impl Field for FieldX2 { + const NUM_FIELDS: usize = 2; + const INDEX: usize = 0; +} + +impl Field for FieldY2 { + const NUM_FIELDS: usize = 2; + const INDEX: usize = 1; +} + +// Fields of Nested1 (generated). +struct FieldX1; +struct FieldY1; + +impl Field for FieldX1 { + const NUM_FIELDS: usize = 2; + const INDEX: usize = 0; +} + +impl Field for FieldY1 { + const NUM_FIELDS: usize = 2; + const INDEX: usize = 1; +} + +// Implementations of Resolve (generated). +impl Resolve for Nested3 { + type Output = Nested2; + + fn gindex(parent_index: usize) -> usize { + parent_index * ::NUM_FIELDS.next_power_of_two() + + ::INDEX + } +} + +impl Resolve for Nested3 { + type Output = Nested1; + + fn gindex(parent_index: usize) -> usize { + parent_index * ::NUM_FIELDS.next_power_of_two() + + ::INDEX + } +} + +impl Resolve for Nested2 { + type Output = Nested1; + + fn gindex(parent_index: usize) -> usize { + parent_index * ::NUM_FIELDS.next_power_of_two() + + ::INDEX + } +} + +impl Resolve for Nested2 { + type Output = Nested1; + + fn gindex(parent_index: usize) -> usize { + parent_index * ::NUM_FIELDS.next_power_of_two() + + ::INDEX + } +} + +impl Resolve for Nested1 { + type Output = u64; + + fn gindex(parent_index: usize) -> usize { + parent_index * ::NUM_FIELDS.next_power_of_two() + + ::INDEX + } +} + +impl Resolve for Nested1 { + type Output = Vec; + + fn gindex(parent_index: usize) -> usize { + parent_index * ::NUM_FIELDS.next_power_of_two() + + ::INDEX + } +} + +// x3.x2.x1 +type FieldX3X2X1 = Path>; + +// x3.x2.x1 +type FieldX3X2Y1 = Path>; + +// This evaluates to u64 at compile-time. +type TypeOfFieldX3X2X1 = >::Output; + +#[test] +fn gindex_basics() { + // This works but just shows compile-time field resolution. + let x: TypeOfFieldX3X2X1 = 0u64; + + // Gindex computation. + assert_eq!(>::gindex(1), 8); + assert_eq!(>::gindex(1), 9); +} + +/* +impl TreeHash for u64 { + fn get_field(&self, _: usize) -> Result<&dyn TreeHash, Error> { + Err(Error::Oops) + } + + fn merkle_proof(&self) +} +*/ From b2baf724a97d54f83a87e675badd2bc06af62104 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 25 Jun 2025 17:25:06 +1000 Subject: [PATCH 2/3] Move tests to ssz_types crate --- tree_hash/Cargo.toml | 1 + tree_hash/src/prototype.rs | 143 +++++-------------------------------- 2 files changed, 19 insertions(+), 125 deletions(-) diff --git a/tree_hash/Cargo.toml b/tree_hash/Cargo.toml index f3e6d35..9c23ea2 100644 --- a/tree_hash/Cargo.toml +++ b/tree_hash/Cargo.toml @@ -21,6 +21,7 @@ typenum = "1.12.0" rand = "0.8.5" tree_hash_derive = { path = "../tree_hash_derive", version = "0.10.0" } ethereum_ssz_derive = "0.9.0" +ssz_types = "0.11.0" [features] arbitrary = ["alloy-primitives/arbitrary"] diff --git a/tree_hash/src/prototype.rs b/tree_hash/src/prototype.rs index 94e8594..8729196 100644 --- a/tree_hash/src/prototype.rs +++ b/tree_hash/src/prototype.rs @@ -11,23 +11,24 @@ trait TreeHash { fn merkle_proof(&self, generalized_index: usize) -> Result, Error>; } */ +use crate::{TreeHash, TreeHashType, BYTES_PER_CHUNK}; use std::marker::PhantomData; // A path is a sequence of field accesses like `self.foo.bar`. // // We can resolve these at compile time, because we're sickos. -struct Path(PhantomData<(First, Rest)>); +pub struct Path(PhantomData<(First, Rest)>); // The field trait is implemented for all struct fields. // // It provides conversion from a named field type to a numeric field index. -trait Field { +pub trait Field { const NUM_FIELDS: usize; const INDEX: usize; } // If T implements Resolve it means T has a field named Field of type Output. -trait Resolve { +pub trait Resolve { type Output; fn gindex(parent_index: usize) -> usize; @@ -50,136 +51,28 @@ where } } -// Some example structs. -struct Nested3 { - x3: Nested2, - y3: Nested1, -} - -struct Nested2 { - x2: Nested1, - y2: Nested1, -} - -struct Nested1 { - x1: u64, - y1: Vec, -} - -// Fields of Nested3 (these would be generated). -struct FieldX3; -struct FieldY3; - -impl Field for FieldX3 { - const NUM_FIELDS: usize = 2; - const INDEX: usize = 0; -} - -impl Field for FieldY3 { - const NUM_FIELDS: usize = 2; - const INDEX: usize = 1; -} - -// Fields of Nested2 (generated). -struct FieldX2; -struct FieldY2; - -impl Field for FieldX2 { - const NUM_FIELDS: usize = 2; - const INDEX: usize = 0; -} - -impl Field for FieldY2 { - const NUM_FIELDS: usize = 2; - const INDEX: usize = 1; -} - -// Fields of Nested1 (generated). -struct FieldX1; -struct FieldY1; - -impl Field for FieldX1 { - const NUM_FIELDS: usize = 2; - const INDEX: usize = 0; -} +pub struct VecIndex; -impl Field for FieldY1 { - const NUM_FIELDS: usize = 2; - const INDEX: usize = 1; +impl Field for VecIndex { + const NUM_FIELDS: usize = N; + const INDEX: usize = I; } -// Implementations of Resolve (generated). -impl Resolve for Nested3 { - type Output = Nested2; - - fn gindex(parent_index: usize) -> usize { - parent_index * ::NUM_FIELDS.next_power_of_two() - + ::INDEX +pub fn item_length() -> usize { + if T::tree_hash_type() == TreeHashType::Basic { + BYTES_PER_CHUNK / T::tree_hash_packing_factor() + } else { + BYTES_PER_CHUNK } } -impl Resolve for Nested3 { - type Output = Nested1; - - fn gindex(parent_index: usize) -> usize { - parent_index * ::NUM_FIELDS.next_power_of_two() - + ::INDEX - } +pub fn vector_chunk_count(length: usize) -> usize { + (length * item_length::()).div_ceil(BYTES_PER_CHUNK) } -impl Resolve for Nested2 { - type Output = Nested1; - - fn gindex(parent_index: usize) -> usize { - parent_index * ::NUM_FIELDS.next_power_of_two() - + ::INDEX - } -} - -impl Resolve for Nested2 { - type Output = Nested1; - - fn gindex(parent_index: usize) -> usize { - parent_index * ::NUM_FIELDS.next_power_of_two() - + ::INDEX - } -} - -impl Resolve for Nested1 { - type Output = u64; - - fn gindex(parent_index: usize) -> usize { - parent_index * ::NUM_FIELDS.next_power_of_two() - + ::INDEX - } -} - -impl Resolve for Nested1 { - type Output = Vec; - - fn gindex(parent_index: usize) -> usize { - parent_index * ::NUM_FIELDS.next_power_of_two() - + ::INDEX - } -} - -// x3.x2.x1 -type FieldX3X2X1 = Path>; - -// x3.x2.x1 -type FieldX3X2Y1 = Path>; - -// This evaluates to u64 at compile-time. -type TypeOfFieldX3X2X1 = >::Output; - -#[test] -fn gindex_basics() { - // This works but just shows compile-time field resolution. - let x: TypeOfFieldX3X2X1 = 0u64; - - // Gindex computation. - assert_eq!(>::gindex(1), 8); - assert_eq!(>::gindex(1), 9); +pub fn get_vector_item_position(index: usize) -> usize { + let start = index * item_length::(); + start / BYTES_PER_CHUNK } /* From f9f95053cbcc96d3c6a1e7dd5cc7dc1582b2e9aa Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 25 Jun 2025 17:56:36 +1000 Subject: [PATCH 3/3] Add sketch of MerkleProof trait --- tree_hash/src/prototype.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tree_hash/src/prototype.rs b/tree_hash/src/prototype.rs index 8729196..4f89e43 100644 --- a/tree_hash/src/prototype.rs +++ b/tree_hash/src/prototype.rs @@ -1,18 +1,21 @@ -/* -type Hash256 = [u8; 32]; +use crate::{Hash256, TreeHash, TreeHashType, BYTES_PER_CHUNK}; +use std::marker::PhantomData; -enum Error { - Oops +pub enum Error { + Oops, } -trait TreeHash { - fn get_field(&self, index: usize) -> Result<&dyn TreeHash, Error>; +pub trait MerkleProof: TreeHash { + fn compute_proof(&self) -> Result, Error> + where + Self: Resolve, + { + let gindex = >::gindex(1); + self.compute_proof_for_gindex(gindex) + } - fn merkle_proof(&self, generalized_index: usize) -> Result, Error>; + fn compute_proof_for_gindex(&self, gindex: usize) -> Result, Error>; } -*/ -use crate::{TreeHash, TreeHashType, BYTES_PER_CHUNK}; -use std::marker::PhantomData; // A path is a sequence of field accesses like `self.foo.bar`. // @@ -51,6 +54,7 @@ where } } +// FIXME: we don't currently enforce I < N at compile-time pub struct VecIndex; impl Field for VecIndex {