Skip to content

Commit cd968fd

Browse files
refactor: blake hash to match the v0.14 cairo`s implementation (#142)
1 parent 66b894f commit cd968fd

File tree

2 files changed

+82
-35
lines changed

2 files changed

+82
-35
lines changed

crates/starknet-types-core/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ papyrus-serialization = ["std"]
5353
zeroize = ["dep:zeroize"]
5454

5555
[dev-dependencies]
56-
proptest = { version = "1.5", default-features = false, features = ["alloc", "proptest-macro"] }
56+
proptest = { version = "1.5", default-features = false, features = ["alloc", "proptest-macro"] }
5757
regex = "1.11"
5858
serde_test = "1"
5959
criterion = "0.5"
6060
rand_chacha = "0.3"
6161
rand = "0.8"
62+
rstest = "0.24"
6263
lazy_static = { version = "1.5", default-features = false }
6364

6465
[[bench]]

crates/starknet-types-core/src/hash/blake2s.rs

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
//! with the MSB of the first word set as a marker
1313
//!
1414
//! The resulting words are serialized in little-endian byte order before hashing.
15-
//! The Blake2s-256 digest is then truncated to 224 bits to fit within a `Felt`.
15+
//! The Blake2s-256 digest is then packed into a `Felt` using all 256 bits modulo the field prime.
1616
//!
1717
//! ## Reference Implementation
1818
//!
1919
//! This implementation follows the Cairo specification:
20-
//! - [Cairo Blake2s implementation](https://github.com/starkware-libs/cairo-lang/blob/ab8be40403a7634ba296c467b26b8bd945ba5cfa/src/starkware/cairo/common/cairo_blake2s/blake2s.cairo)
20+
//! - [Cairo Blake2s implementation](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/cairo_blake2s/blake2s.cairo)
2121
2222
use crate::felt::Felt;
2323
use blake2::Blake2s256;
@@ -36,7 +36,7 @@ impl StarkHash for Blake2Felt252 {
3636
}
3737

3838
fn hash_array(felts: &[Felt]) -> Felt {
39-
Self::encode_felt252_data_and_calc_224_bit_blake_hash(felts)
39+
Self::encode_felt252_data_and_calc_blake_hash(felts)
4040
}
4141

4242
fn hash_single(felt: &Felt) -> Felt {
@@ -84,14 +84,14 @@ impl Blake2Felt252 {
8484
unpacked_u32s
8585
}
8686

87-
/// Packs the first 7 little-endian 32-bit words (28 bytes) of `bytes`
88-
/// into a single 224-bit Felt.
89-
fn pack_224_le_to_felt(bytes: &[u8]) -> Felt {
90-
assert!(bytes.len() >= 28, "need at least 28 bytes to pack 7 words");
87+
/// Packs the first 8 little-endian 32-bit words (32 bytes) of `bytes`
88+
/// into a single 252-bit Felt.
89+
fn pack_256_le_to_felt(bytes: &[u8]) -> Felt {
90+
assert!(bytes.len() >= 32, "need at least 32 bytes to pack 8 words");
9191

92-
// 1) copy your 28-byte LE-hash into the low 28 bytes of a 32-byte buffer.
92+
// 1) copy your 32-byte LE-hash into the low 32 bytes of a 32-byte buffer.
9393
let mut buf = [0u8; 32];
94-
buf[..28].copy_from_slice(&bytes[..28]);
94+
buf[..32].copy_from_slice(&bytes[..32]);
9595

9696
// 2) interpret the whole 32-byte buffer as a little-endian Felt.
9797
Felt::from_bytes_le(&buf)
@@ -101,14 +101,14 @@ impl Blake2Felt252 {
101101
let mut hasher = Blake2s256::new();
102102
hasher.update(data);
103103
let hash32 = hasher.finalize();
104-
Self::pack_224_le_to_felt(hash32.as_slice())
104+
Self::pack_256_le_to_felt(hash32.as_slice())
105105
}
106106

107107
/// Encodes a slice of `Felt` values into 32-bit words exactly as Cairo's
108108
/// [`encode_felt252_to_u32s`](https://github.com/starkware-libs/cairo-lang/blob/ab8be40403a7634ba296c467b26b8bd945ba5cfa/src/starkware/cairo/common/cairo_blake2s/blake2s.cairo)
109109
/// hint does, then hashes the resulting byte stream with Blake2s-256 and
110-
/// returns the 224-bit truncated digest as a `Felt`.
111-
pub fn encode_felt252_data_and_calc_224_bit_blake_hash(data: &[Felt]) -> Felt {
110+
/// returns the full 256-bit digest as a `Felt`.
111+
pub fn encode_felt252_data_and_calc_blake_hash(data: &[Felt]) -> Felt {
112112
// 1) Unpack each Felt into 2 or 8 u32.
113113
let u32_words = Self::encode_felts_to_u32s(data);
114114

@@ -127,6 +127,7 @@ impl Blake2Felt252 {
127127
mod tests {
128128
use super::*;
129129
use crate::felt::Felt;
130+
use rstest::rstest;
130131

131132
/// Test two-limb encoding for a small Felt (< 2^63) into high and low 32-bit words.
132133
#[test]
@@ -154,49 +155,94 @@ mod tests {
154155
assert_eq!(words, expected);
155156
}
156157

157-
/// Test packing of a 28-byte little-endian buffer into a 224-bit Felt.
158+
/// Test packing of a 32-byte little-endian buffer into a 256-bit Felt.
158159
#[test]
159-
fn test_pack_224_le_to_felt_roundtrip() {
160-
// Create a 28-byte buffer with values 1..28 and pad to 32 bytes.
160+
fn test_pack_256_le_to_felt_basic() {
161+
// Test with small values that won't trigger modular reduction.
161162
let mut buf = [0u8; 32];
162-
for i in 0..28 {
163-
buf[i] = (i + 1) as u8;
164-
}
165-
let f = Blake2Felt252::pack_224_le_to_felt(&buf);
163+
buf[0] = 0x01;
164+
buf[1] = 0x02;
165+
buf[2] = 0x03;
166+
buf[3] = 0x04;
167+
// Leave the rest as zeros.
168+
169+
let f = Blake2Felt252::pack_256_le_to_felt(&buf);
166170
let out = f.to_bytes_le();
167171

168-
// Low 28 bytes must match the input buffer.
169-
assert_eq!(&out[..28], &buf[..28]);
170-
// High 4 bytes must remain zero.
171-
assert_eq!(&out[28..], &[0, 0, 0, 0]);
172+
// For small values, the first few bytes should match exactly.
173+
assert_eq!(out[0], 0x01);
174+
assert_eq!(out[1], 0x02);
175+
assert_eq!(out[2], 0x03);
176+
assert_eq!(out[3], 0x04);
177+
178+
// Test that the packing formula works correctly for a simple case.
179+
let expected = Felt::from(0x01)
180+
+ Felt::from(0x02) * Felt::from(1u64 << 8)
181+
+ Felt::from(0x03) * Felt::from(1u64 << 16)
182+
+ Felt::from(0x04) * Felt::from(1u64 << 24);
183+
assert_eq!(f, expected);
184+
185+
// Test with a value that exceeds the field prime P to verify modular reduction.
186+
// Create a 32-byte buffer with all 0xFF bytes, representing 2^256 - 1.
187+
let max_buf = [0xFF_u8; 32];
188+
let f_max = Blake2Felt252::pack_256_le_to_felt(&max_buf);
189+
190+
// The result should be (2^256 - 1) mod P.
191+
// Since 2^256 = Felt::TWO.pow(256), we can compute this value directly.
192+
// This tests that modular reduction works correctly when exceeding the field prime.
193+
let two_pow_256_minus_one = Felt::TWO.pow(256u32) - Felt::ONE;
194+
assert_eq!(f_max, two_pow_256_minus_one);
172195
}
173196

174-
/// Test that pack_224_le_to_felt panics when input is shorter than 28 bytes.
197+
/// Test that pack_256_le_to_felt panics when input is shorter than 32 bytes.
175198
#[test]
176-
#[should_panic(expected = "need at least 28 bytes")]
177-
fn test_pack_224_le_to_felt_too_short() {
178-
let too_short = [0u8; 27];
179-
Blake2Felt252::pack_224_le_to_felt(&too_short);
199+
#[should_panic(expected = "need at least 32 bytes to pack 8 words")]
200+
fn test_pack_256_le_to_felt_too_short() {
201+
let too_short = [0u8; 31];
202+
Blake2Felt252::pack_256_le_to_felt(&too_short);
180203
}
181204

182-
/// Test that hashing a single zero Felt produces the expected 224-bit Blake2s digest.
205+
/// Test that hashing a single zero Felt produces the expected 256-bit Blake2s digest.
183206
#[test]
184207
fn test_hash_single_zero() {
185208
let zero = Felt::from_hex_unchecked("0");
186209
let hash = Blake2Felt252::hash_single(&zero);
187-
let expected =
188-
Felt::from_hex_unchecked("71a2f9bc7c9df9dc4ca0e7a1c5908d5eff88af963c3264f412dbdf50");
210+
let expected = Felt::from_hex_unchecked(
211+
"5768af071a2f8df7c9df9dc4ca0e7a1c5908d5eff88af963c3264f412dbdf43",
212+
);
189213
assert_eq!(hash, expected);
190214
}
191215

192-
/// Test that hashing an array of Felts [1, 2] produces the expected 224-bit Blake2s digest.
216+
/// Test that hashing an array of Felts [1, 2] produces the expected 256-bit Blake2s digest.
193217
#[test]
194218
fn test_hash_array_one_two() {
195219
let one = Felt::from_hex_unchecked("1");
196220
let two = Felt::from_hex_unchecked("2");
197221
let hash = Blake2Felt252::hash_array(&[one, two]);
198-
let expected =
199-
Felt::from_hex_unchecked("a14b223236366f30e9c77b6e56c8835de7dc5aee36957d4384cce67b");
222+
let expected = Felt::from_hex_unchecked(
223+
"5534c03a14b214436366f30e9c77b6e56c8835de7dc5aee36957d4384cce66d",
224+
);
200225
assert_eq!(hash, expected);
201226
}
227+
228+
/// Test the encode_felt252_data_and_calc_blake_hash function
229+
/// with the same result as the Cairo v0.14 version.
230+
#[rstest]
231+
#[case::empty(vec![], "874258848688468311465623299960361657518391155660316941922502367727700287818")]
232+
#[case::boundary_under_2_63(vec![Felt::from((1u64 << 63) - 1)], "94160078030592802631039216199460125121854007413180444742120780261703604445")]
233+
#[case::boundary_at_2_63(vec![Felt::from(1u64 << 63)], "318549634615606806810268830802792194529205864650702991817600345489579978482")]
234+
#[case::very_large_felt(vec![Felt::from_hex_unchecked("800000000000011000000000000000000000000000000000000000000000000")], "3505594194634492896230805823524239179921427575619914728883524629460058657521")]
235+
#[case::mixed_small_large(vec![Felt::from(42), Felt::from(1u64 << 63), Felt::from(1337)], "1127477916086913892828040583976438888091205536601278656613505514972451246501")]
236+
#[case::max_u64(vec![Felt::from(u64::MAX)], "3515074221976790747383295076946184515593027667350620348239642126105984996390")]
237+
fn test_encode_felt252_data_and_calc_blake_hash(
238+
#[case] input: Vec<Felt>,
239+
#[case] expected_result: &str,
240+
) {
241+
let result = Blake2Felt252::encode_felt252_data_and_calc_blake_hash(&input);
242+
let expected = Felt::from_dec_str(expected_result).unwrap();
243+
assert_eq!(
244+
result, expected,
245+
"rust_implementation: {result:?} != cairo_implementation: {expected:?}"
246+
);
247+
}
202248
}

0 commit comments

Comments
 (0)