@@ -9,6 +9,88 @@ use math::{
99 traits:: AsBytes ,
1010} ;
1111
12+ #[ cfg( target_arch = "riscv64" ) ]
13+ use crate :: hash:: platform_keccak:: PlatformKeccak256 ;
14+ #[ cfg( target_arch = "riscv64" ) ]
15+ use core:: any:: TypeId ;
16+ #[ cfg( target_arch = "riscv64" ) ]
17+ use lambda_vm_syscalls:: keccak:: Keccak256 as SyscallKeccak256 ;
18+
19+ /// Absorb `feed`'s byte stream into a fresh `D` and return the digest as a
20+ /// fixed `[u8; NUM_BYTES]`.
21+ ///
22+ /// On the riscv64 guest, when `D` is the platform keccak digest and the node
23+ /// is 32 bytes, this drives the syscall sponge directly and squeezes straight
24+ /// into the result array. That skips the `Digest::finalize` blanket, which
25+ /// allocates a zeroed `GenericArray`, has the adapter fill a local `[u8; 32]`
26+ /// and copy it into that `Output`, then leaves the caller to copy the `Output`
27+ /// once more into its own array — two 32-byte memcpys plus a 32-byte memset of
28+ /// pure plumbing around the one permutation. Byte-identical to the generic
29+ /// path; every other digest / node size (and the entire host build) takes the
30+ /// generic path unchanged.
31+ ///
32+ /// DO NOT replace this `TypeId` dispatch with a generic `Digest::finalize_into`
33+ /// fix "at the adapter altitude" — that exact refactor was implemented and
34+ /// MEASURED SLOWER on the guest across every formulation tried (best:
35+ /// +60k min = +0.14%, +1.25M blowup8 = +0.48%), including `#[inline]`
36+ /// adapters and a check-free `AsMut` output conversion. The residual is
37+ /// intrinsic: `FixedOutput::finalize_into` moves the 208-byte sponge by value
38+ /// through the newtype + trait layer into a non-inlined cross-crate call, and
39+ /// without LTO the placement isn't elided; the direct branch below builds the
40+ /// sponge in place at the call's ABI slot. Deleting the dispatch also cannot
41+ /// remove the `'static` bounds — `hash_new_parent_bytes` needs them regardless.
42+ #[ inline]
43+ fn hash_streamed < D : Digest + ' static , const NUM_BYTES : usize > (
44+ feed : impl Fn ( & mut dyn FnMut ( & [ u8 ] ) ) ,
45+ ) -> [ u8 ; NUM_BYTES ] {
46+ #[ cfg( target_arch = "riscv64" ) ]
47+ if NUM_BYTES == 32 && TypeId :: of :: < D > ( ) == TypeId :: of :: < PlatformKeccak256 > ( ) {
48+ let mut hasher = SyscallKeccak256 :: new ( ) ;
49+ feed ( & mut |bytes| hasher. update ( bytes) ) ;
50+ let mut result = [ 0u8 ; NUM_BYTES ] ;
51+ // NUM_BYTES == 32 in this branch, so the slice is exactly a [u8; 32].
52+ let out: & mut [ u8 ; 32 ] = ( & mut result[ ..] ) . try_into ( ) . unwrap ( ) ;
53+ hasher. finalize ( out) ;
54+ return result;
55+ }
56+
57+ let mut hasher = D :: new ( ) ;
58+ feed ( & mut |bytes| hasher. update ( bytes) ) ;
59+ let mut result_hash = [ 0_u8 ; NUM_BYTES ] ;
60+ result_hash. copy_from_slice ( & hasher. finalize ( ) ) ;
61+ result_hash
62+ }
63+
64+ /// Hash a Merkle parent — always exactly two concatenated 32-byte nodes.
65+ ///
66+ /// On the riscv64 guest, when `D` is the platform keccak digest and nodes are
67+ /// 32 bytes, this is one fixed-shape 64-byte compression ([`keccak256_pair`]):
68+ /// a single permutation with the input lanes and padding written straight into
69+ /// the state, skipping the incremental sponge's per-byte absorb, running
70+ /// offset, and separate padding pass. Byte-identical to streaming both nodes
71+ /// through the digest; every other digest / node size (and the host build)
72+ /// takes the generic streaming-and-finalize path unchanged.
73+ #[ inline]
74+ fn hash_new_parent_bytes < D : Digest + ' static , const NUM_BYTES : usize > (
75+ left : & [ u8 ; NUM_BYTES ] ,
76+ right : & [ u8 ; NUM_BYTES ] ,
77+ ) -> [ u8 ; NUM_BYTES ] {
78+ #[ cfg( target_arch = "riscv64" ) ]
79+ if NUM_BYTES == 32 && TypeId :: of :: < D > ( ) == TypeId :: of :: < PlatformKeccak256 > ( ) {
80+ let l: & [ u8 ; 32 ] = left[ ..] . try_into ( ) . unwrap ( ) ;
81+ let r: & [ u8 ; 32 ] = right[ ..] . try_into ( ) . unwrap ( ) ;
82+ let hash = lambda_vm_syscalls:: keccak:: keccak256_pair ( l, r) ;
83+ let mut result = [ 0u8 ; NUM_BYTES ] ;
84+ result. copy_from_slice ( & hash) ;
85+ return result;
86+ }
87+
88+ hash_streamed :: < D , NUM_BYTES > ( |sink| {
89+ sink ( left) ;
90+ sink ( right) ;
91+ } )
92+ }
93+
1294/// A backend for Merkle trees that uses fixed-size pairs of field elements.
1395/// This is more efficient than `FieldElementVectorBackend` when the batch size is always 2,
1496/// as it avoids Vec allocation overhead.
@@ -27,7 +109,7 @@ impl<F, D: Digest, const NUM_BYTES: usize> Default for FieldElementPairBackend<F
27109 }
28110}
29111
30- impl < F , D : Digest , const NUM_BYTES : usize > IsMerkleTreeBackend
112+ impl < F , D : Digest + ' static , const NUM_BYTES : usize > IsMerkleTreeBackend
31113 for FieldElementPairBackend < F , D , NUM_BYTES >
32114where
33115 F : IsField ,
@@ -38,21 +120,14 @@ where
38120 type Data = [ FieldElement < F > ; 2 ] ;
39121
40122 fn hash_data ( input : & [ FieldElement < F > ; 2 ] ) -> [ u8 ; NUM_BYTES ] {
41- let mut hasher = D :: new ( ) ;
42- input[ 0 ] . stream_bytes ( & mut |b| hasher. update ( b) ) ;
43- input[ 1 ] . stream_bytes ( & mut |b| hasher. update ( b) ) ;
44- let mut result_hash = [ 0_u8 ; NUM_BYTES ] ;
45- result_hash. copy_from_slice ( & hasher. finalize ( ) ) ;
46- result_hash
123+ hash_streamed :: < D , NUM_BYTES > ( |sink| {
124+ input[ 0 ] . stream_bytes ( sink) ;
125+ input[ 1 ] . stream_bytes ( sink) ;
126+ } )
47127 }
48128
49129 fn hash_new_parent ( left : & [ u8 ; NUM_BYTES ] , right : & [ u8 ; NUM_BYTES ] ) -> [ u8 ; NUM_BYTES ] {
50- let mut hasher = D :: new ( ) ;
51- hasher. update ( left) ;
52- hasher. update ( right) ;
53- let mut result_hash = [ 0_u8 ; NUM_BYTES ] ;
54- result_hash. copy_from_slice ( & hasher. finalize ( ) ) ;
55- result_hash
130+ hash_new_parent_bytes :: < D , NUM_BYTES > ( left, right)
56131 }
57132}
58133
@@ -71,7 +146,7 @@ impl<F, D: Digest, const NUM_BYTES: usize> Default for FieldElementVectorBackend
71146 }
72147}
73148
74- impl < F , D : Digest , const NUM_BYTES : usize > FieldElementVectorBackend < F , D , NUM_BYTES >
149+ impl < F , D : Digest + ' static , const NUM_BYTES : usize > FieldElementVectorBackend < F , D , NUM_BYTES >
75150where
76151 [ u8 ; NUM_BYTES ] : From < Output < D > > ,
77152{
@@ -80,15 +155,11 @@ where
80155 /// once, avoiding per-element allocations while staying consistent with the
81156 /// backend's hash function.
82157 pub fn hash_bytes ( data : & [ u8 ] ) -> [ u8 ; NUM_BYTES ] {
83- let mut hasher = D :: new ( ) ;
84- hasher. update ( data) ;
85- let mut result = [ 0u8 ; NUM_BYTES ] ;
86- result. copy_from_slice ( & hasher. finalize ( ) ) ;
87- result
158+ hash_streamed :: < D , NUM_BYTES > ( |sink| sink ( data) )
88159 }
89160}
90161
91- impl < F , D : Digest , const NUM_BYTES : usize > FieldElementVectorBackend < F , D , NUM_BYTES >
162+ impl < F , D : Digest + ' static , const NUM_BYTES : usize > FieldElementVectorBackend < F , D , NUM_BYTES >
92163where
93164 F : IsField ,
94165 FieldElement < F > : AsBytes ,
@@ -100,17 +171,15 @@ where
100171 /// `hash_data(&[a, b].concat())`: the sponge absorbs the same element bytes
101172 /// in the same order, just without the intermediate `Vec`.
102173 pub fn hash_data_from_slices ( a : & [ FieldElement < F > ] , b : & [ FieldElement < F > ] ) -> [ u8 ; NUM_BYTES ] {
103- let mut hasher = D :: new ( ) ;
104- for element in a. iter ( ) . chain ( b. iter ( ) ) {
105- element. stream_bytes ( & mut |bytes| hasher. update ( bytes) ) ;
106- }
107- let mut result_hash = [ 0_u8 ; NUM_BYTES ] ;
108- result_hash. copy_from_slice ( & hasher. finalize ( ) ) ;
109- result_hash
174+ hash_streamed :: < D , NUM_BYTES > ( |sink| {
175+ for element in a. iter ( ) . chain ( b. iter ( ) ) {
176+ element. stream_bytes ( sink) ;
177+ }
178+ } )
110179 }
111180}
112181
113- impl < F , D : Digest , const NUM_BYTES : usize > IsMerkleTreeBackend
182+ impl < F , D : Digest + ' static , const NUM_BYTES : usize > IsMerkleTreeBackend
114183 for FieldElementVectorBackend < F , D , NUM_BYTES >
115184where
116185 F : IsField ,
@@ -129,12 +198,7 @@ where
129198 }
130199
131200 fn hash_new_parent ( left : & [ u8 ; NUM_BYTES ] , right : & [ u8 ; NUM_BYTES ] ) -> [ u8 ; NUM_BYTES ] {
132- let mut hasher = D :: new ( ) ;
133- hasher. update ( left) ;
134- hasher. update ( right) ;
135- let mut result_hash = [ 0_u8 ; NUM_BYTES ] ;
136- result_hash. copy_from_slice ( & hasher. finalize ( ) ) ;
137- result_hash
201+ hash_new_parent_bytes :: < D , NUM_BYTES > ( left, right)
138202 }
139203}
140204
0 commit comments