Skip to content

Commit 5dce514

Browse files
authored
feat: #[derive(LightHasher)] macro and AsByteVec trait (#1085)
* feat: `#[derive(LightHasher)]` macro and `AsByteVec` trait - `AsByteVec`, providing `as_byte_vec()` method, guarantees consistent way of serializing types to 2D byte vectors before hashing. We provide default implementations for primitives. More complex types need to implement `IntoBytes` themselves. - By using 2D vectors, we make it possible to represent structs with multiple fields. This way, we can handle struct fields (nested structs) the same way as primitive fields and deal with all types by using one trait. - The reason behing using vectors instead of slices is that in some cases, we cannot just cast the type without defining custom bytes. Vectors, although they might introduce copies, make sure we always own the bytes we are creating. - `#[derive(LightHasher)]` implements `AsByteVec` and `DataHasher` traits. The `DataHasher` implementation takes bytes returned by `AsByteVec` as an input. * test: Add more test cases for the `AsByteVec` trait - empty string - `Option` with `Some` and `None` and assertion that `None != Some(0)` - array (including an empty one) * doc: Add documentation to `LightHasher` macro
1 parent 673e30b commit 5dce514

File tree

8 files changed

+600
-0
lines changed

8 files changed

+600
-0
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

macros/light/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ syn = { version = "1.0", features = ["full"] }
1414

1515
light-hasher = { path = "../../merkle-tree/hasher", version = "0.3.0" }
1616

17+
[dev-dependencies]
18+
light-utils = { path = "../../utils", version = "0.3.0" }
19+
1720
[lib]
1821
proc-macro = true

macros/light/src/hasher.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use proc_macro2::TokenStream;
2+
use quote::quote;
3+
use syn::{Error, Fields, ItemStruct, Result};
4+
5+
pub(crate) fn hasher(input: ItemStruct) -> Result<TokenStream> {
6+
let struct_name = &input.ident;
7+
8+
let (impl_gen, type_gen, where_clause) = input.generics.split_for_impl();
9+
10+
let fields = match input.fields {
11+
Fields::Named(fields) => fields,
12+
_ => {
13+
return Err(Error::new_spanned(
14+
input,
15+
"Only structs with named fields are supported",
16+
))
17+
}
18+
};
19+
20+
let field_into_bytes_calls = fields
21+
.named
22+
.iter()
23+
.filter(|field| {
24+
!field.attrs.iter().any(|attr| {
25+
if let Some(attr_ident) = attr.path.get_ident() {
26+
attr_ident == "skip"
27+
} else {
28+
false
29+
}
30+
})
31+
})
32+
.map(|field| {
33+
let field_name = &field.ident;
34+
let truncate = field.attrs.iter().any(|attr| {
35+
if let Some(attr_ident) = attr.path.get_ident() {
36+
attr_ident == "truncate"
37+
} else {
38+
false
39+
}
40+
});
41+
if truncate {
42+
quote! {
43+
let truncated_bytes = self
44+
.#field_name
45+
.as_byte_vec()
46+
.iter()
47+
.map(|bytes| {
48+
let (bytes, _) = ::light_utils::hash_to_bn254_field_size_be(bytes).expect(
49+
"Could not truncate the field #field_name to the BN254 prime field"
50+
);
51+
bytes.to_vec()
52+
})
53+
.collect::<Vec<Vec<u8>>>();
54+
result.extend_from_slice(truncated_bytes.as_slice());
55+
}
56+
} else {
57+
quote! {
58+
result.extend_from_slice(self.#field_name.as_byte_vec().as_slice());
59+
}
60+
}
61+
})
62+
.collect::<Vec<_>>();
63+
64+
Ok(quote! {
65+
impl #impl_gen ::light_hasher::bytes::AsByteVec for #struct_name #type_gen #where_clause {
66+
fn as_byte_vec(&self) -> Vec<Vec<u8>> {
67+
use ::light_hasher::bytes::AsByteVec;
68+
69+
let mut result: Vec<Vec<u8>> = Vec::new();
70+
#(#field_into_bytes_calls)*
71+
result
72+
}
73+
}
74+
75+
impl #impl_gen ::light_hasher::DataHasher for #struct_name #type_gen #where_clause {
76+
fn hash<H: light_hasher::Hasher>(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::errors::HasherError> {
77+
use ::light_hasher::bytes::AsByteVec;
78+
79+
H::hashv(self.as_byte_vec().iter().map(|v| v.as_slice()).collect::<Vec<_>>().as_slice())
80+
}
81+
}
82+
})
83+
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use super::*;
88+
89+
use syn::parse_quote;
90+
91+
#[test]
92+
fn test_light_hasher() {
93+
let input: ItemStruct = parse_quote! {
94+
struct MyAccount {
95+
a: u32,
96+
b: i32,
97+
c: u64,
98+
d: i64,
99+
}
100+
};
101+
102+
let output = hasher(input).unwrap();
103+
let output = output.to_string();
104+
105+
println!("{output}");
106+
}
107+
}

macros/light/src/lib.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use traits::process_light_traits;
77

88
mod accounts;
99
mod discriminator;
10+
mod hasher;
1011
mod pubkey;
1112
mod traits;
1213

@@ -161,3 +162,119 @@ pub fn light_discriminator(input: TokenStream) -> TokenStream {
161162
.unwrap_or_else(|err| err.to_compile_error())
162163
.into()
163164
}
165+
166+
/// Makes the annotated struct hashable by implementing the following traits:
167+
///
168+
/// - [`AsByteVec`](light_hasher::bytes::AsByteVec), which makes the struct
169+
/// convertable to a 2D byte vector.
170+
/// - [`DataHasher`](light_hasher::DataHasher), which makes the struct hashable
171+
/// with the `hash()` method, based on the byte inputs from `AsByteVec`
172+
/// implementation.
173+
///
174+
/// This macro assumes that all the fields of the struct implement the
175+
/// `AsByteVec` trait. The trait is implemented by default for the most of
176+
/// standard Rust types (primitives, `String`, arrays and options carrying the
177+
/// former). If there is a field of a type not implementing the trait, there
178+
/// are two options:
179+
///
180+
/// 1. The most recommended one - annotating that type with the `light_hasher`
181+
/// macro as well.
182+
/// 2. Manually implementing the `AsByteVec` trait.
183+
///
184+
/// # Attributes
185+
///
186+
/// - `skip` - skips the given field, it doesn't get included neither in
187+
/// `AsByteVec` nor `DataHasher` implementation.
188+
/// - `truncate` - makes sure that the byte value does not exceed the BN254
189+
/// prime field modulus, by hashing it (with Keccak) and truncating it to 31
190+
/// bytes. It's generally a good idea to use it on any field which is
191+
/// expected to output more than 31 bytes.
192+
///
193+
/// # Examples
194+
///
195+
/// Compressed account with only primitive types as fields:
196+
///
197+
/// ```ignore
198+
/// #[derive(LightHasher)]
199+
/// pub struct MyCompressedAccount {
200+
/// a: i64,
201+
/// b: Option<u64>,
202+
/// }
203+
/// ```
204+
///
205+
/// Compressed account with fields which might exceed the BN254 prime field:
206+
///
207+
/// ```ignore
208+
/// #[derive(LightHasher)]
209+
/// pub struct MyCompressedAccount {
210+
/// a: i64
211+
/// b: Option<u64>,
212+
/// #[truncate]
213+
/// c: [u8; 32],
214+
/// #[truncate]
215+
/// d: String,
216+
/// }
217+
/// ```
218+
///
219+
/// Compressed account with fields we want to skip:
220+
///
221+
/// ```ignore
222+
/// #[derive(LightHasher)]
223+
/// pub struct MyCompressedAccount {
224+
/// a: i64
225+
/// b: Option<u64>,
226+
/// #[skip]
227+
/// c: [u8; 32],
228+
/// }
229+
/// ```
230+
///
231+
/// Compressed account with a nested struct:
232+
///
233+
/// ```ignore
234+
/// #[derive(LightHasher)]
235+
/// pub struct MyCompressedAccount {
236+
/// a: i64
237+
/// b: Option<u64>,
238+
/// c: MyStruct,
239+
/// }
240+
///
241+
/// #[derive(LightHasher)]
242+
/// pub struct MyStruct {
243+
/// a: i32
244+
/// b: u32,
245+
/// }
246+
/// ```
247+
///
248+
/// Compressed account with a type with a custom `AsByteVec` implementation:
249+
///
250+
/// ```ignore
251+
/// #[derive(LightHasher)]
252+
/// pub struct MyCompressedAccount {
253+
/// a: i64
254+
/// b: Option<u64>,
255+
/// c: RData,
256+
/// }
257+
///
258+
/// pub enum RData {
259+
/// A(Ipv4Addr),
260+
/// AAAA(Ipv6Addr),
261+
/// CName(String),
262+
/// }
263+
///
264+
/// impl AsByteVec for RData {
265+
/// fn as_byte_vec(&self) -> Vec<Vec<u8>> {
266+
/// match self {
267+
/// Self::A(ipv4_addr) => vec![ipv4_addr.octets().to_vec()],
268+
/// Self::AAAA(ipv6_addr) => vec![ipv6_addr.octets().to_vec()],
269+
/// Self::CName(cname) => cname.as_byte_vec(),
270+
/// }
271+
/// }
272+
/// }
273+
/// ```
274+
#[proc_macro_derive(LightHasher, attributes(skip, truncate))]
275+
pub fn light_hasher(input: TokenStream) -> TokenStream {
276+
let input = parse_macro_input!(input as ItemStruct);
277+
hasher::hasher(input)
278+
.unwrap_or_else(|err| err.to_compile_error())
279+
.into()
280+
}

macros/light/tests/hasher.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use std::{cell::RefCell, marker::PhantomData, rc::Rc};
2+
3+
use light_hasher::{DataHasher, Poseidon};
4+
use light_macros::LightHasher;
5+
6+
#[derive(LightHasher)]
7+
pub struct MyAccount {
8+
pub a: bool,
9+
pub b: u64,
10+
pub c: MyNestedStruct,
11+
#[truncate]
12+
pub d: [u8; 32],
13+
#[skip]
14+
pub e: MyNestedNonHashableStruct,
15+
pub f: Option<usize>,
16+
}
17+
18+
#[derive(LightHasher)]
19+
pub struct MyNestedStruct {
20+
pub a: i32,
21+
pub b: u32,
22+
#[truncate]
23+
pub c: String,
24+
}
25+
26+
pub struct MyNestedNonHashableStruct {
27+
pub a: PhantomData<()>,
28+
pub b: Rc<RefCell<usize>>,
29+
}
30+
31+
#[test]
32+
fn test_light_hasher() {
33+
let my_account = MyAccount {
34+
a: true,
35+
b: u64::MAX,
36+
c: MyNestedStruct {
37+
a: i32::MIN,
38+
b: u32::MAX,
39+
c: "wao".to_string(),
40+
},
41+
d: [u8::MAX; 32],
42+
e: MyNestedNonHashableStruct {
43+
a: PhantomData,
44+
b: Rc::new(RefCell::new(usize::MAX)),
45+
},
46+
f: None,
47+
};
48+
assert_eq!(
49+
my_account.hash::<Poseidon>().unwrap(),
50+
[
51+
44, 62, 31, 169, 73, 125, 135, 126, 176, 7, 127, 96, 183, 224, 156, 140, 105, 77, 225,
52+
230, 174, 196, 38, 92, 0, 44, 19, 25, 255, 109, 6, 168
53+
]
54+
);
55+
56+
let my_account = MyAccount {
57+
a: true,
58+
b: u64::MAX,
59+
c: MyNestedStruct {
60+
a: i32::MIN,
61+
b: u32::MAX,
62+
c: "wao".to_string(),
63+
},
64+
d: [u8::MAX; 32],
65+
e: MyNestedNonHashableStruct {
66+
a: PhantomData,
67+
b: Rc::new(RefCell::new(usize::MAX)),
68+
},
69+
f: Some(0),
70+
};
71+
assert_eq!(
72+
my_account.hash::<Poseidon>().unwrap(),
73+
[
74+
32, 205, 141, 227, 236, 5, 28, 219, 24, 164, 215, 79, 151, 131, 162, 82, 224, 101, 171,
75+
201, 4, 181, 26, 146, 6, 1, 95, 107, 239, 19, 233, 80
76+
]
77+
);
78+
}

merkle-tree/hasher/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ thiserror = "1.0"
1818
ark-bn254 = "0.4.0"
1919
sha2 = "0.10"
2020
sha3 = "0.10"
21+
22+
[dev-dependencies]
23+
rand = "0.8"

0 commit comments

Comments
 (0)