Skip to main content

ethrex_trie/
nibbles.rs

1#[cfg(not(feature = "std"))]
2use alloc::vec::Vec;
3use core::{cmp, mem};
4
5use ethrex_rlp::{
6    decode::RLPDecode,
7    encode::RLPEncode,
8    error::RLPDecodeError,
9    structs::{Decoder, Encoder},
10};
11
12// ── SIMD nibble expansion ────────────────────────────────────────────────────
13//
14// The hot path during block execution converts 32-byte keccak keys to 64
15// nibbles on every trie lookup / insert.  We replace the original flat_map
16// iterator chain with a SIMD kernel that processes 16 or 32 bytes per cycle.
17
18/// Expands each byte in `bytes` into two nibbles (high nibble first),
19/// writing `bytes.len() * 2` bytes to the uninitialized `output` pointer.
20///
21/// # Safety
22/// `output` must be valid for writes of at least `bytes.len() * 2` bytes.
23#[inline]
24#[allow(unsafe_code)]
25unsafe fn expand_bytes_to_nibbles(bytes: &[u8], output: *mut u8) {
26    #[cfg(target_arch = "x86_64")]
27    {
28        // SAFETY: caller guarantees output is writable for bytes.len() * 2 bytes.
29        unsafe { expand_bytes_to_nibbles_x86_64(bytes, output) };
30        return;
31    }
32    #[cfg(target_arch = "aarch64")]
33    {
34        // SAFETY: caller guarantees output is writable for bytes.len() * 2 bytes.
35        unsafe { expand_bytes_to_nibbles_aarch64(bytes, output) };
36        return;
37    }
38    // Portable scalar fallback for other architectures.
39    #[allow(unreachable_code)]
40    // SAFETY: caller guarantees output is writable for bytes.len() * 2 bytes.
41    unsafe {
42        expand_bytes_to_nibbles_scalar(bytes, output)
43    };
44}
45
46#[cfg(target_arch = "x86_64")]
47#[allow(unsafe_code)]
48#[inline]
49unsafe fn expand_bytes_to_nibbles_x86_64(bytes: &[u8], output: *mut u8) {
50    use core::arch::x86_64::*;
51
52    let n = bytes.len();
53    let mut i = 0usize;
54
55    // --- AVX2 path: 32 bytes → 64 nibbles per iteration ---
56    // Enabled only when the compiler has +avx2 in target-feature
57    // (set via .cargo/config.toml for the production x86_64-linux target).
58    #[cfg(target_feature = "avx2")]
59    // SAFETY: AVX2 is enabled at compile time via .cargo/config.toml target-feature.
60    unsafe {
61        let mask256 = _mm256_set1_epi8(0x0F_u8 as i8);
62        while i + 32 <= n {
63            // Load 32 input bytes.
64            let v = _mm256_loadu_si256(bytes.as_ptr().add(i).cast::<__m256i>());
65            // Extract high nibbles: shift each 16-bit word right by 4, then mask.
66            let hi = _mm256_and_si256(_mm256_srli_epi16(v, 4), mask256);
67            // Extract low nibbles.
68            let lo = _mm256_and_si256(v, mask256);
69            // Interleave hi/lo within each 128-bit lane.
70            // _mm256_unpacklo_epi8 → [hi0,lo0,hi1,lo1,…,hi7,lo7 | hi16,lo16,…,hi23,lo23]
71            // _mm256_unpackhi_epi8 → [hi8,lo8,…,hi15,lo15       | hi24,lo24,…,hi31,lo31]
72            let unpack_lo = _mm256_unpacklo_epi8(hi, lo);
73            let unpack_hi = _mm256_unpackhi_epi8(hi, lo);
74            // Cross-lane permute to restore sequential byte order:
75            //   out_lo = [lane0(unpack_lo), lane0(unpack_hi)] = bytes  0-15 nibbles
76            //   out_hi = [lane1(unpack_lo), lane1(unpack_hi)] = bytes 16-31 nibbles
77            let out_lo = _mm256_permute2x128_si256::<0x20>(unpack_lo, unpack_hi);
78            let out_hi = _mm256_permute2x128_si256::<0x31>(unpack_lo, unpack_hi);
79            _mm256_storeu_si256(output.add(i * 2).cast::<__m256i>(), out_lo);
80            _mm256_storeu_si256(output.add(i * 2 + 32).cast::<__m256i>(), out_hi);
81            i += 32;
82        }
83    }
84
85    // --- SSE2 path: 16 bytes → 32 nibbles per iteration ---
86    // SSE2 is part of the x86_64 baseline; no runtime check needed.
87    // SAFETY: SSE2 is always available on x86_64; pointer arithmetic stays within bounds.
88    unsafe {
89        let mask128 = _mm_set1_epi8(0x0F_u8 as i8);
90        while i + 16 <= n {
91            let v = _mm_loadu_si128(bytes.as_ptr().add(i).cast::<__m128i>());
92            let hi = _mm_and_si128(_mm_srli_epi16(v, 4), mask128);
93            let lo = _mm_and_si128(v, mask128);
94            let lo16 = _mm_unpacklo_epi8(hi, lo);
95            let hi16 = _mm_unpackhi_epi8(hi, lo);
96            _mm_storeu_si128(output.add(i * 2).cast::<__m128i>(), lo16);
97            _mm_storeu_si128(output.add(i * 2 + 16).cast::<__m128i>(), hi16);
98            i += 16;
99        }
100
101        // Scalar tail for remaining bytes (0-15).
102        while i < n {
103            let b = *bytes.get_unchecked(i);
104            *output.add(i * 2) = b >> 4;
105            *output.add(i * 2 + 1) = b & 0x0F;
106            i += 1;
107        }
108    }
109}
110
111#[cfg(target_arch = "aarch64")]
112#[target_feature(enable = "neon")]
113#[allow(unsafe_code)]
114#[inline]
115unsafe fn expand_bytes_to_nibbles_aarch64(bytes: &[u8], output: *mut u8) {
116    use core::arch::aarch64::*;
117
118    let n = bytes.len();
119    let mut i = 0usize;
120
121    // NEON is mandatory on aarch64; no runtime detection needed.
122    // SAFETY: NEON is always available on aarch64; bounds are maintained by the loop guard.
123    unsafe {
124        let mask_0f = vdupq_n_u8(0x0F);
125        while i + 16 <= n {
126            let v = vld1q_u8(bytes.as_ptr().add(i));
127            // vshrq_n_u8 shifts each 8-bit lane independently → high nibbles directly.
128            let hi = vshrq_n_u8(v, 4);
129            let lo = vandq_u8(v, mask_0f);
130            // vzip1q_u8 / vzip2q_u8 interleave bytes from two vectors.
131            let lo16 = vzip1q_u8(hi, lo); // [hi0,lo0,hi1,lo1,…,hi7,lo7]
132            let hi16 = vzip2q_u8(hi, lo); // [hi8,lo8,…,hi15,lo15]
133            vst1q_u8(output.add(i * 2), lo16);
134            vst1q_u8(output.add(i * 2 + 16), hi16);
135            i += 16;
136        }
137
138        // Scalar tail.
139        while i < n {
140            let b = *bytes.get_unchecked(i);
141            *output.add(i * 2) = b >> 4;
142            *output.add(i * 2 + 1) = b & 0x0F;
143            i += 1;
144        }
145    }
146}
147
148#[allow(unsafe_code)]
149#[inline]
150unsafe fn expand_bytes_to_nibbles_scalar(bytes: &[u8], output: *mut u8) {
151    // SAFETY: caller guarantees output is valid for bytes.len() * 2 bytes.
152    unsafe {
153        for (i, &b) in bytes.iter().enumerate() {
154            *output.add(i * 2) = b >> 4;
155            *output.add(i * 2 + 1) = b & 0x0F;
156        }
157    }
158}
159
160// ── SIMD nibble packing ──────────────────────────────────────────────────────
161//
162// pack_nibble_pairs combines pairs of nibbles [hi, lo, hi, lo, …] into bytes
163// [(hi<<4)|lo, …].  This is the hot path inside encode_compact, called on every
164// trie node when computing the Merkle root.
165//
166// Strategy: use SSSE3 _mm_maddubs_epi16 which does
167//   result[i] = a[2i]*16 + a[2i+1]  (treating a as unsigned, b as signed)
168// Setting b = [16, 1, 16, 1, …] gives the packed nibble byte for each pair.
169// This is enabled when SSSE3 is available (always on x86-64-v3).
170
171/// Packs pairs of nibbles in `nibbles` into bytes, writing to `output`.
172/// `nibbles.len()` must be even.
173///
174/// # Safety
175/// `output` must be writable for `nibbles.len() / 2` bytes.
176#[inline]
177#[allow(unsafe_code)]
178unsafe fn pack_nibble_pairs(nibbles: &[u8], output: *mut u8) {
179    debug_assert!(nibbles.len().is_multiple_of(2));
180    #[cfg(target_arch = "x86_64")]
181    {
182        unsafe { pack_nibble_pairs_x86_64(nibbles, output) };
183        return;
184    }
185    #[cfg(target_arch = "aarch64")]
186    {
187        unsafe { pack_nibble_pairs_aarch64(nibbles, output) };
188        return;
189    }
190    #[allow(unreachable_code)]
191    unsafe {
192        pack_nibble_pairs_scalar(nibbles, output)
193    };
194}
195
196#[cfg(target_arch = "x86_64")]
197#[allow(unsafe_code)]
198#[inline]
199unsafe fn pack_nibble_pairs_x86_64(nibbles: &[u8], output: *mut u8) {
200    let n = nibbles.len(); // always even
201    let mut i = 0usize; // index into nibbles (steps of 32)
202    let mut o = 0usize; // index into output (steps of 16)
203
204    // SSSE3 path: 32 nibbles → 16 output bytes per iteration.
205    // _mm_maddubs_epi16(a, b): result[k] = (a[2k]*b[2k] + a[2k+1]*b[2k+1]) as i16
206    // With b=[16,1,16,1,...] and a=[hi,lo,...]: result[k] = hi*16 + lo
207    #[cfg(target_feature = "ssse3")]
208    // SAFETY: SSSE3 enabled at compile time; pointer arithmetic stays within bounds.
209    unsafe {
210        use core::arch::x86_64::*;
211        // Multiplier: weight = [16, 1] repeated → multiply even nibble by 16, odd by 1
212        let weights = _mm_set1_epi16(0x0110_u16 as i16); // bytes: [16, 1, 16, 1, ...]
213        while i + 32 <= n {
214            // Load 32 nibbles (16 pairs) in two 128-bit chunks.
215            let lo_chunk = _mm_loadu_si128(nibbles.as_ptr().add(i).cast::<__m128i>());
216            let hi_chunk = _mm_loadu_si128(nibbles.as_ptr().add(i + 16).cast::<__m128i>());
217            // maddubs: [hi0*16+lo0, hi1*16+lo1, …, hi7*16+lo7] as 16-bit lanes
218            let lo_packed = _mm_maddubs_epi16(lo_chunk, weights);
219            let hi_packed = _mm_maddubs_epi16(hi_chunk, weights);
220            // packus: saturate to u8 and pack both 8×i16 → 16×u8
221            let result = _mm_packus_epi16(lo_packed, hi_packed);
222            _mm_storeu_si128(output.add(o).cast::<__m128i>(), result);
223            i += 32;
224            o += 16;
225        }
226    }
227
228    // Scalar tail (handles remaining pairs when n is not a multiple of 32,
229    // or when SSSE3 is unavailable).
230    unsafe {
231        while i + 2 <= n {
232            *output.add(o) = (*nibbles.get_unchecked(i) << 4) | *nibbles.get_unchecked(i + 1);
233            i += 2;
234            o += 1;
235        }
236    }
237}
238
239#[cfg(target_arch = "aarch64")]
240#[target_feature(enable = "neon")]
241#[allow(unsafe_code)]
242#[inline]
243unsafe fn pack_nibble_pairs_aarch64(nibbles: &[u8], output: *mut u8) {
244    use core::arch::aarch64::*;
245
246    let n = nibbles.len();
247    let mut i = 0usize;
248    let mut o = 0usize;
249
250    // SAFETY: NEON always available; bounds maintained by loop guard.
251    unsafe {
252        while i + 32 <= n {
253            // Load 32 nibbles interleaved as [hi, lo] pairs.
254            let v = vld2q_u8(nibbles.as_ptr().add(i));
255            // v.0 = hi nibbles, v.1 = lo nibbles
256            // Pack: (hi << 4) | lo
257            let packed = vorrq_u8(vshlq_n_u8(v.0, 4), v.1);
258            vst1q_u8(output.add(o), packed);
259            i += 32;
260            o += 16;
261        }
262        while i + 2 <= n {
263            *output.add(o) = (*nibbles.get_unchecked(i) << 4) | *nibbles.get_unchecked(i + 1);
264            i += 2;
265            o += 1;
266        }
267    }
268}
269
270#[allow(unsafe_code)]
271#[inline]
272unsafe fn pack_nibble_pairs_scalar(nibbles: &[u8], output: *mut u8) {
273    // SAFETY: caller ensures `output` is valid for nibbles.len()/2 bytes.
274    unsafe {
275        let mut o = 0usize;
276        let mut i = 0usize;
277        let n = nibbles.len();
278        while i + 2 <= n {
279            *output.add(o) = (*nibbles.get_unchecked(i) << 4) | *nibbles.get_unchecked(i + 1);
280            i += 2;
281            o += 1;
282        }
283    }
284}
285// ─────────────────────────────────────────────────────────────────────────────
286
287// ── SIMD prefix comparison ───────────────────────────────────────────────────
288//
289// count_common_prefix finds the length of the longest common prefix of two
290// byte slices.  The trie uses this on every insert/lookup to navigate branch
291// nodes.  Using SIMD we can compare 16 (SSE2) or 32 (AVX2) bytes at once.
292
293#[allow(unsafe_code)]
294#[inline]
295fn count_common_prefix(a: &[u8], b: &[u8]) -> usize {
296    #[cfg(target_arch = "x86_64")]
297    {
298        // SAFETY: x86_64 SIMD; bounds are maintained within the function.
299        return unsafe { count_common_prefix_x86_64(a, b) };
300    }
301    #[cfg(target_arch = "aarch64")]
302    {
303        // SAFETY: NEON enabled; bounds are maintained within the function.
304        return unsafe { count_common_prefix_aarch64(a, b) };
305    }
306    #[allow(unreachable_code)]
307    count_common_prefix_scalar(a, b)
308}
309
310#[cfg(target_arch = "x86_64")]
311#[allow(unsafe_code)]
312#[inline]
313unsafe fn count_common_prefix_x86_64(a: &[u8], b: &[u8]) -> usize {
314    use core::arch::x86_64::*;
315
316    let n = a.len().min(b.len());
317    let mut i = 0usize;
318
319    #[cfg(target_feature = "avx2")]
320    // SAFETY: AVX2 enabled at compile time; pointer arithmetic stays within bounds.
321    unsafe {
322        while i + 32 <= n {
323            let va = _mm256_loadu_si256(a.as_ptr().add(i).cast::<__m256i>());
324            let vb = _mm256_loadu_si256(b.as_ptr().add(i).cast::<__m256i>());
325            // Compare bytes: equal → 0xFF, else 0x00
326            let eq = _mm256_cmpeq_epi8(va, vb);
327            // Create a 32-bit mask where bit k = 1 iff byte k was equal.
328            let mask = _mm256_movemask_epi8(eq) as u32;
329            if mask != 0xFFFF_FFFF {
330                // First differing byte is at bit position (trailing ones).
331                return i + mask.trailing_ones() as usize;
332            }
333            i += 32;
334        }
335    }
336
337    // SSE2 (16-byte chunks). SSE2 is x86_64 baseline.
338    // SAFETY: SSE2 always available; bounds maintained by loop guard.
339    unsafe {
340        while i + 16 <= n {
341            let va = _mm_loadu_si128(a.as_ptr().add(i).cast::<__m128i>());
342            let vb = _mm_loadu_si128(b.as_ptr().add(i).cast::<__m128i>());
343            let eq = _mm_cmpeq_epi8(va, vb);
344            let mask = _mm_movemask_epi8(eq) as u16;
345            if mask != 0xFFFF {
346                return i + mask.trailing_ones() as usize;
347            }
348            i += 16;
349        }
350    }
351
352    // Scalar tail.
353    i + count_common_prefix_scalar(&a[i..n], &b[i..n])
354}
355
356#[cfg(target_arch = "aarch64")]
357#[target_feature(enable = "neon")]
358#[allow(unsafe_code)]
359#[inline]
360unsafe fn count_common_prefix_aarch64(a: &[u8], b: &[u8]) -> usize {
361    use core::arch::aarch64::*;
362
363    let n = a.len().min(b.len());
364    let mut i = 0usize;
365
366    // SAFETY: NEON always available; pointer arithmetic stays within bounds.
367    unsafe {
368        while i + 16 <= n {
369            let va = vld1q_u8(a.as_ptr().add(i));
370            let vb = vld1q_u8(b.as_ptr().add(i));
371            // vceqq_u8: equal lanes → 0xFF, else 0x00
372            let eq = vceqq_u8(va, vb);
373            // vminvq_u8: reduce to minimum; if all 0xFF then all bytes matched.
374            if vminvq_u8(eq) == 0xFF {
375                i += 16;
376                continue;
377            }
378            // Find first non-matching byte by scanning the 16-byte window.
379            let mut eq_arr = [0u8; 16];
380            vst1q_u8(eq_arr.as_mut_ptr(), eq);
381            for (j, &byte) in eq_arr.iter().enumerate() {
382                if byte == 0 {
383                    return i + j;
384                }
385            }
386            unreachable!()
387        }
388    }
389
390    i + count_common_prefix_scalar(&a[i..n], &b[i..n])
391}
392
393#[inline]
394fn count_common_prefix_scalar(a: &[u8], b: &[u8]) -> usize {
395    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
396}
397// ─────────────────────────────────────────────────────────────────────────────
398
399// TODO: move path-tracking logic somewhere else
400// PERF: try using a stack-allocated array
401/// Struct representing a list of nibbles (half-bytes)
402#[derive(
403    Debug,
404    Clone,
405    Default,
406    serde::Serialize,
407    serde::Deserialize,
408    rkyv::Deserialize,
409    rkyv::Serialize,
410    rkyv::Archive,
411)]
412pub struct Nibbles {
413    data: Vec<u8>,
414    /// Parts of the path that have already been consumed (used for tracking
415    /// current position when visiting nodes). See `current()`.
416    already_consumed: Vec<u8>,
417}
418
419// NOTE: custom impls to ignore the `already_consumed` field
420
421impl PartialEq for Nibbles {
422    fn eq(&self, other: &Nibbles) -> bool {
423        self.data == other.data
424    }
425}
426
427impl Eq for Nibbles {}
428
429impl PartialOrd for Nibbles {
430    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
431        Some(self.cmp(other))
432    }
433}
434
435impl Ord for Nibbles {
436    fn cmp(&self, other: &Self) -> cmp::Ordering {
437        self.data.cmp(&other.data)
438    }
439}
440
441impl core::hash::Hash for Nibbles {
442    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
443        self.data.hash(state);
444    }
445}
446
447impl Nibbles {
448    /// Create `Nibbles` from  hex-encoded nibbles
449    pub const fn from_hex(hex: Vec<u8>) -> Self {
450        Self {
451            data: hex,
452            already_consumed: vec![],
453        }
454    }
455
456    /// Splits incoming bytes into nibbles and appends the leaf flag (a 16 nibble at the end)
457    pub fn from_bytes(bytes: &[u8]) -> Self {
458        Self::from_raw(bytes, true)
459    }
460
461    /// Splits incoming bytes into nibbles and appends the leaf flag (a 16 nibble at the end) if is_leaf is true
462    pub fn from_raw(bytes: &[u8], is_leaf: bool) -> Self {
463        let extra = usize::from(is_leaf);
464        let mut data = Vec::with_capacity(bytes.len() * 2 + extra);
465
466        // SAFETY: we allocated at least `bytes.len() * 2` capacity (plus `extra`),
467        // and we set_len to exactly `bytes.len() * 2` after the SIMD kernel fills them.
468        #[allow(unsafe_code)]
469        unsafe {
470            expand_bytes_to_nibbles(bytes, data.as_mut_ptr());
471            data.set_len(bytes.len() * 2);
472        }
473
474        if is_leaf {
475            data.push(16);
476        }
477
478        Self {
479            data,
480            already_consumed: vec![],
481        }
482    }
483
484    pub fn into_vec(self) -> Vec<u8> {
485        self.data
486    }
487
488    /// Returns the amount of nibbles
489    pub fn len(&self) -> usize {
490        self.data.len()
491    }
492
493    /// Returns true if there are no nibbles
494    pub fn is_empty(&self) -> bool {
495        self.data.is_empty()
496    }
497
498    /// If `prefix` is a prefix of self, move the offset after
499    /// the prefix and return true, otherwise return false.
500    pub fn skip_prefix(&mut self, prefix: &Nibbles) -> bool {
501        if self.len() >= prefix.len() && &self.data[..prefix.len()] == prefix.as_ref() {
502            self.already_consumed.extend_from_slice(&prefix.data);
503            self.data.drain(..prefix.len());
504            true
505        } else {
506            false
507        }
508    }
509
510    /// Compares self to another, comparing prefixes only in case of unequal lengths.
511    pub fn compare_prefix(&self, prefix: &Nibbles) -> cmp::Ordering {
512        if self.len() > prefix.len() {
513            self.data[..prefix.len()].cmp(&prefix.data)
514        } else {
515            self.data[..].cmp(&prefix.data[..self.len()])
516        }
517    }
518
519    /// Compares self to another and returns the shared nibble count (amount of nibbles that are equal, from the start)
520    pub fn count_prefix(&self, other: &Nibbles) -> usize {
521        count_common_prefix(self.as_ref(), other.as_ref())
522    }
523
524    /// Removes and returns the first nibble
525    #[allow(clippy::should_implement_trait)]
526    pub fn next(&mut self) -> Option<u8> {
527        (!self.is_empty()).then(|| {
528            self.already_consumed.push(self.data[0]);
529            self.data.remove(0)
530        })
531    }
532
533    /// Removes and returns the first nibble if it is a suitable choice index (aka < 16)
534    pub fn next_choice(&mut self) -> Option<usize> {
535        self.next().filter(|choice| *choice < 16).map(usize::from)
536    }
537
538    /// Returns the nibbles after the given offset
539    pub fn offset(&self, offset: usize) -> Nibbles {
540        let mut already_consumed = Vec::with_capacity(self.already_consumed.len() + offset);
541        already_consumed.extend_from_slice(&self.already_consumed);
542        already_consumed.extend_from_slice(&self.data[..offset]);
543        Nibbles {
544            data: self.data[offset..].to_vec(),
545            already_consumed,
546        }
547    }
548
549    /// Returns the nibbles beween the start and end indexes
550    pub fn slice(&self, start: usize, end: usize) -> Nibbles {
551        Nibbles::from_hex(self.data[start..end].to_vec())
552    }
553
554    /// Extends the nibbles with another list of nibbles
555    pub fn extend(&mut self, other: &Nibbles) {
556        self.data.extend_from_slice(other.as_ref());
557    }
558
559    /// Return the nibble at the given index, will panic if the index is out of range
560    pub fn at(&self, i: usize) -> usize {
561        self.data[i] as usize
562    }
563
564    /// Inserts a nibble at the start
565    pub fn prepend(&mut self, nibble: u8) {
566        self.data.insert(0, nibble);
567    }
568
569    /// Inserts a nibble at the end
570    pub fn append(&mut self, nibble: u8) {
571        self.data.push(nibble);
572    }
573
574    /// Taken from https://github.com/citahub/cita_trie/blob/master/src/nibbles.rs#L56
575    /// Encodes the nibbles in compact form
576    #[allow(unsafe_code)]
577    pub fn encode_compact(&self) -> Vec<u8> {
578        let is_leaf = self.is_leaf();
579        let mut hex = if is_leaf {
580            &self.data[0..self.data.len() - 1]
581        } else {
582            &self.data[0..]
583        };
584        // node type    path length    |    prefix    hexchar
585        // --------------------------------------------------
586        // extension    even           |    0000      0x0
587        // extension    odd            |    0001      0x1
588        // leaf         even           |    0010      0x2
589        // leaf         odd            |    0011      0x3
590        let prefix_nibble = if hex.len() % 2 == 1 {
591            let v = 0x10 + hex[0];
592            hex = &hex[1..];
593            v
594        } else {
595            0x00
596        };
597
598        let pair_count = hex.len() / 2;
599        let mut compact = Vec::with_capacity(1 + pair_count);
600        compact.push(prefix_nibble + if is_leaf { 0x20 } else { 0x00 });
601
602        // SIMD-accelerated packing of nibble pairs → bytes.
603        // SAFETY: compact has capacity for `pair_count` bytes beyond the one already pushed.
604        // pack_nibble_pairs writes exactly `pair_count` bytes starting at offset 1;
605        // set_len then exposes those initialized bytes.
606        unsafe {
607            let out_ptr = compact.as_mut_ptr().add(1);
608            pack_nibble_pairs(hex, out_ptr);
609            compact.set_len(1 + pair_count);
610        }
611
612        compact
613    }
614
615    /// Encodes the nibbles in compact form
616    pub fn decode_compact(compact: &[u8]) -> Self {
617        Self::from_hex(compact_to_hex(compact))
618    }
619
620    /// Returns true if the nibbles contain the leaf flag (16) at the end
621    pub fn is_leaf(&self) -> bool {
622        if self.is_empty() {
623            false
624        } else {
625            self.data[self.data.len() - 1] == 16
626        }
627    }
628
629    /// Combines the nibbles into bytes, trimming the leaf flag if necessary
630    pub fn to_bytes(&self) -> Vec<u8> {
631        // Trim leaf flag
632        let data = if !self.is_empty() && self.is_leaf() {
633            &self.data[..self.len() - 1]
634        } else {
635            &self.data[..]
636        };
637        // Combine nibbles into bytes
638        data.chunks(2)
639            .map(|chunk| match chunk.len() {
640                1 => chunk[0] << 4,
641                _ => chunk[0] << 4 | chunk[1],
642            })
643            .collect::<Vec<_>>()
644    }
645
646    /// Concatenates self and another Nibbles returning a new Nibbles
647    pub fn concat(&self, other: &Nibbles) -> Nibbles {
648        let mut data = Vec::with_capacity(self.data.len() + other.data.len());
649        data.extend_from_slice(&self.data);
650        data.extend_from_slice(&other.data);
651        Nibbles {
652            data,
653            already_consumed: self.already_consumed.clone(),
654        }
655    }
656
657    /// Returns a copy of self with the nibble added at the end
658    pub fn append_new(&self, nibble: u8) -> Nibbles {
659        let mut data = Vec::with_capacity(self.data.len() + 1);
660        data.extend_from_slice(&self.data);
661        data.push(nibble);
662        Nibbles {
663            data,
664            already_consumed: self.already_consumed.clone(),
665        }
666    }
667
668    /// Return already consumed parts of path
669    pub fn current(&self) -> Nibbles {
670        Nibbles {
671            data: self.already_consumed.clone(),
672            already_consumed: vec![],
673        }
674    }
675
676    /// Empties `self.data` and returns the content
677    pub fn take(&mut self) -> Self {
678        Nibbles {
679            data: mem::take(&mut self.data),
680            already_consumed: mem::take(&mut self.already_consumed),
681        }
682    }
683}
684
685impl AsRef<[u8]> for Nibbles {
686    fn as_ref(&self) -> &[u8] {
687        &self.data
688    }
689}
690
691impl RLPEncode for Nibbles {
692    fn encode(&self, buf: &mut dyn bytes::BufMut) {
693        Encoder::new(buf).encode_field(&self.data).finish();
694    }
695}
696
697impl RLPDecode for Nibbles {
698    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
699        let decoder = Decoder::new(rlp)?;
700        let (data, decoder) = decoder.decode_field("data")?;
701        Ok((
702            Self {
703                data,
704                already_consumed: vec![],
705            },
706            decoder.finish()?,
707        ))
708    }
709}
710
711// Code taken from https://github.com/ethereum/go-ethereum/blob/a1093d98eb3260f2abf340903c2d968b2b891c11/trie/encoding.go#L82
712fn compact_to_hex(compact: &[u8]) -> Vec<u8> {
713    if compact.is_empty() {
714        return vec![];
715    }
716    let mut base = keybytes_to_hex(compact);
717    // delete terminator flag
718    let end = if base[0] < 2 {
719        base.len() - 1
720    } else {
721        base.len()
722    };
723    // apply odd flag
724    let chop = 2 - (base[0] & 1) as usize;
725    base.drain(..chop);
726    base.truncate(end - chop);
727    base
728}
729
730// Code taken from https://github.com/ethereum/go-ethereum/blob/a1093d98eb3260f2abf340903c2d968b2b891c11/trie/encoding.go#L96
731fn keybytes_to_hex(keybytes: &[u8]) -> Vec<u8> {
732    let nibble_count = keybytes.len() * 2;
733    let mut nibbles = Vec::with_capacity(nibble_count + 1);
734
735    // SAFETY: we just allocated `nibble_count` capacity; SIMD kernel fills them.
736    #[allow(unsafe_code)]
737    unsafe {
738        expand_bytes_to_nibbles(keybytes, nibbles.as_mut_ptr());
739        nibbles.set_len(nibble_count);
740    }
741    nibbles.push(16); // leaf terminator
742    nibbles
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    /// Scalar reference for expand_bytes_to_nibbles (no SIMD).
750    fn expand_bytes_scalar_ref(bytes: &[u8]) -> Vec<u8> {
751        bytes.iter().flat_map(|&b| [b >> 4, b & 0x0F]).collect()
752    }
753
754    /// Scalar reference for pack_nibble_pairs (no SIMD).
755    fn pack_nibble_pairs_scalar_ref(nibbles: &[u8]) -> Vec<u8> {
756        nibbles
757            .chunks_exact(2)
758            .map(|pair| (pair[0] << 4) | pair[1])
759            .collect()
760    }
761
762    #[test]
763    fn expand_bytes_to_nibbles_matches_scalar() {
764        // Test edge-case lengths: 0, 1, 15, 16, 17, 31, 32, 33, 64
765        for &len in &[0, 1, 2, 15, 16, 17, 31, 32, 33, 48, 64] {
766            let input: Vec<u8> = (0..len).map(|i| (i * 37 + 13) as u8).collect();
767            let expected = expand_bytes_scalar_ref(&input);
768
769            let mut actual = vec![0u8; input.len() * 2];
770            #[allow(unsafe_code)]
771            unsafe {
772                expand_bytes_to_nibbles(&input, actual.as_mut_ptr());
773            }
774            assert_eq!(actual, expected, "mismatch at input length {len}");
775        }
776    }
777
778    #[test]
779    fn pack_nibble_pairs_matches_scalar() {
780        // Test edge-case pair counts: 0, 2, 14, 16, 30, 32, 34, 64
781        for &nibble_count in &[0, 2, 4, 14, 16, 30, 32, 34, 48, 64] {
782            let input: Vec<u8> = (0..nibble_count).map(|i| (i % 16) as u8).collect();
783            let expected = pack_nibble_pairs_scalar_ref(&input);
784
785            let mut actual = vec![0u8; nibble_count / 2];
786            #[allow(unsafe_code)]
787            unsafe {
788                pack_nibble_pairs(&input, actual.as_mut_ptr());
789            }
790            assert_eq!(actual, expected, "mismatch at nibble count {nibble_count}");
791        }
792    }
793
794    #[test]
795    fn expand_then_pack_roundtrip() {
796        for &len in &[0, 1, 16, 32, 33] {
797            let input: Vec<u8> = (0..len).map(|i| (i * 53 + 7) as u8).collect();
798            let mut nibbles = vec![0u8; input.len() * 2];
799            #[allow(unsafe_code)]
800            unsafe {
801                expand_bytes_to_nibbles(&input, nibbles.as_mut_ptr());
802            }
803
804            let mut packed = vec![0u8; input.len()];
805            #[allow(unsafe_code)]
806            unsafe {
807                pack_nibble_pairs(&nibbles, packed.as_mut_ptr());
808            }
809            assert_eq!(packed, input, "roundtrip failed at length {len}");
810        }
811    }
812
813    #[test]
814    fn count_common_prefix_correctness() {
815        // Identical slices
816        let a = vec![1u8, 2, 3, 4, 5];
817        assert_eq!(count_common_prefix(&a, &a), 5);
818
819        // No common prefix
820        assert_eq!(count_common_prefix(&[1, 2, 3], &[4, 5, 6]), 0);
821
822        // Partial match
823        assert_eq!(count_common_prefix(&[1, 2, 3, 4], &[1, 2, 5, 6]), 2);
824
825        // Empty
826        assert_eq!(count_common_prefix(&[], &[1, 2]), 0);
827        assert_eq!(count_common_prefix(&[1, 2], &[]), 0);
828        assert_eq!(count_common_prefix(&[], &[]), 0);
829
830        // Long match crossing SIMD boundaries (>16 bytes)
831        let long_a: Vec<u8> = (0..33).collect();
832        let mut long_b = long_a.clone();
833        long_b[32] = 255;
834        assert_eq!(count_common_prefix(&long_a, &long_b), 32);
835    }
836
837    #[test]
838    fn from_raw_leaf_flag() {
839        let bytes = &[0xAB, 0xCD];
840        let with_leaf = Nibbles::from_raw(bytes, true);
841        let without_leaf = Nibbles::from_raw(bytes, false);
842
843        assert_eq!(with_leaf.data, vec![0x0A, 0x0B, 0x0C, 0x0D, 16]);
844        assert_eq!(without_leaf.data, vec![0x0A, 0x0B, 0x0C, 0x0D]);
845    }
846}