Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: move path encoding from nybbles #14

Merged
merged 4 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
rust: ["stable", "beta", "nightly", "1.65"] # MSRV
rust: ["stable", "beta", "nightly", "1.66"] # MSRV
flags: ["--no-default-features", "", "--all-features"]
steps:
- uses: actions/checkout@v3
Expand All @@ -29,7 +29,7 @@ jobs:
- name: build
run: cargo build --workspace ${{ matrix.flags }}
- name: test
if: ${{ matrix.rust != '1.65' }} # MSRV
if: ${{ matrix.rust != '1.66' }} # MSRV
run: cargo test --workspace ${{ matrix.flags }}

miri:
Expand Down
10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Fast Merkle-Patricia Trie (MPT) state root calculator
and proof generator for prefix-sorted nibbles
"""
edition = "2021"
rust-version = "1.65"
rust-version = "1.66"
license = "MIT OR Apache-2.0"
categories = ["data-structures", "no-std"]
keywords = ["nibbles", "trie", "mpt", "merkle", "ethereum"]
Expand All @@ -25,6 +25,7 @@ alloy-rlp = { version = "0.3", default-features = false, features = ["derive"] }
derive_more = "0.99"
hashbrown = { version = "0.14", features = ["ahash", "inline-more"] }
nybbles = { version = "0.2", default-features = false }
smallvec = { version = "1.0", default-features = false, features = ["const_new"] }
tracing = { version = "0.1", default-features = false }

# serde
Expand All @@ -40,6 +41,7 @@ proptest-derive = { version = "0.4", optional = true }
hash-db = "0.15"
plain_hasher = "0.2"
triehash = "0.8.4"
criterion = "0.5"

[features]
default = ["std"]
Expand All @@ -52,4 +54,10 @@ arbitrary = [
"dep:proptest",
"dep:proptest-derive",
"alloy-primitives/arbitrary",
"nybbles/arbitrary",
]

[[bench]]
name = "bench"
harness = false
required-features = ["arbitrary"]
39 changes: 39 additions & 0 deletions benches/bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use alloy_trie::nodes::encode_path_leaf;
use criterion::{
criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion,
};
use nybbles::Nibbles;
use proptest::{prelude::*, strategy::ValueTree};
use std::{hint::black_box, time::Duration};

/// Benchmarks the nibble path encoding.
pub fn nibbles_path_encoding(c: &mut Criterion) {
let lengths = [16u64, 32, 256, 2048];

let mut g = group(c, "encode_path_leaf");
for len in lengths {
g.throughput(criterion::Throughput::Bytes(len));
let id = criterion::BenchmarkId::new("trie", len);
g.bench_function(id, |b| {
let nibbles = get_nibbles(len as usize);
b.iter(|| black_box(encode_path_leaf(&nibbles, false)))
});
}
}

fn group<'c>(c: &'c mut Criterion, name: &str) -> BenchmarkGroup<'c, WallTime> {
let mut g = c.benchmark_group(name);
g.warm_up_time(Duration::from_secs(1));
g.noise_threshold(0.02);
g
}

fn get_nibbles(len: usize) -> Nibbles {
proptest::arbitrary::any_with::<Nibbles>(len.into())
.new_tree(&mut Default::default())
.unwrap()
.current()
}

criterion_group!(benches, nibbles_path_encoding);
criterion_main!(benches);
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
msrv = "1.65"
msrv = "1.66"
117 changes: 117 additions & 0 deletions src/nodes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use alloy_primitives::{keccak256, Bytes, B256};
use alloy_rlp::{length_of_length, Buf, Decodable, Encodable, Header, EMPTY_STRING_CODE};
use core::ops::Range;
use nybbles::Nibbles;
use smallvec::SmallVec;

#[allow(unused_imports)]
use alloc::vec::Vec;
Expand Down Expand Up @@ -173,6 +174,82 @@ pub(crate) fn unpack_path_to_nibbles(first: Option<u8>, rest: &[u8]) -> Nibbles
Nibbles::from_vec_unchecked(nibbles)
}

/// Encodes a given path leaf as a compact array of bytes, where each byte represents two
/// "nibbles" (half-bytes or 4 bits) of the original hex data, along with additional information
/// about the leaf itself.
///
/// The method takes the following input:
/// `is_leaf`: A boolean value indicating whether the current node is a leaf node or not.
///
/// The first byte of the encoded vector is set based on the `is_leaf` flag and the parity of
/// the hex data length (even or odd number of nibbles).
/// - If the node is an extension with even length, the header byte is `0x00`.
/// - If the node is an extension with odd length, the header byte is `0x10 + <first nibble>`.
/// - If the node is a leaf with even length, the header byte is `0x20`.
/// - If the node is a leaf with odd length, the header byte is `0x30 + <first nibble>`.
///
/// If there is an odd number of nibbles, store the first nibble in the lower 4 bits of the
/// first byte of encoded.
///
/// # Returns
///
/// A vector containing the compact byte representation of the nibble sequence, including the
/// header byte.
///
/// This vector's length is `self.len() / 2 + 1`. For stack-allocated nibbles, this is at most
/// 33 bytes, so 36 was chosen as the stack capacity to round up to the next usize-aligned
/// size.
///
/// # Examples
///
/// ```
/// # use nybbles::Nibbles;
/// // Extension node with an even path length:
/// let nibbles = Nibbles::from_nibbles(&[0x0A, 0x0B, 0x0C, 0x0D]);
/// assert_eq!(nibbles.encode_path_leaf(false)[..], [0x00, 0xAB, 0xCD]);
///
/// // Extension node with an odd path length:
/// let nibbles = Nibbles::from_nibbles(&[0x0A, 0x0B, 0x0C]);
/// assert_eq!(nibbles.encode_path_leaf(false)[..], [0x1A, 0xBC]);
///
/// // Leaf node with an even path length:
/// let nibbles = Nibbles::from_nibbles(&[0x0A, 0x0B, 0x0C, 0x0D]);
/// assert_eq!(nibbles.encode_path_leaf(true)[..], [0x20, 0xAB, 0xCD]);
///
/// // Leaf node with an odd path length:
/// let nibbles = Nibbles::from_nibbles(&[0x0A, 0x0B, 0x0C]);
/// assert_eq!(nibbles.encode_path_leaf(true)[..], [0x3A, 0xBC]);
/// ```
#[inline]
pub fn encode_path_leaf(nibbles: &Nibbles, is_leaf: bool) -> SmallVec<[u8; 36]> {
let encoded_len = nibbles.len() / 2 + 1;
let mut encoded = SmallVec::with_capacity(encoded_len);
// SAFETY: enough capacity.
unsafe { encode_path_leaf_to(nibbles, is_leaf, encoded.as_mut_ptr()) };
// SAFETY: within capacity and `encode_path_leaf_to` initialized the memory.
unsafe { encoded.set_len(encoded_len) };
encoded
}

/// # Safety
///
/// `ptr` must be valid for at least `self.len() / 2 + 1` bytes.
#[inline]
unsafe fn encode_path_leaf_to(nibbles: &Nibbles, is_leaf: bool, ptr: *mut u8) {
let odd_nibbles = nibbles.len() % 2 != 0;
*ptr = match (is_leaf, odd_nibbles) {
(true, true) => LeafNode::ODD_FLAG | nibbles[0],
(true, false) => LeafNode::EVEN_FLAG,
(false, true) => ExtensionNode::ODD_FLAG | nibbles[0],
(false, false) => ExtensionNode::EVEN_FLAG,
};
let mut nibble_idx = if odd_nibbles { 1 } else { 0 };
for i in 0..nibbles.len() / 2 {
ptr.add(i + 1).write(nibbles.get_byte_unchecked(nibble_idx));
nibble_idx += 2;
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -215,4 +292,44 @@ mod tests {
assert_eq!(rlp, hex!("f90211a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a01717171717171717171717171717171717171717171717171717171717171717a0171717171717171717171717171717171717171717171717171717171717171780"));
assert_eq!(TrieNode::decode(&mut &rlp[..]).unwrap(), branch);
}

#[test]
fn hashed_encode_path_regression() {
let nibbles = Nibbles::from_nibbles(hex!("05010406040a040203030f010805020b050c04070003070e0909070f010b0a0805020301070c0a0902040b0f000f0006040a04050f020b090701000a0a040b"));
let path = encode_path_leaf(&nibbles, true);
let expected = hex!("351464a4233f1852b5c47037e997f1ba852317ca924bf0f064a45f2b9710aa4b");
assert_eq!(path[..], expected);
}

#[test]
#[cfg(feature = "arbitrary")]
#[cfg_attr(miri, ignore = "no proptest")]
fn encode_path_first_byte() {
use proptest::{collection::vec, prelude::*};

proptest::proptest!(|(input in vec(any::<u8>(), 1..64))| {
prop_assume!(!input.is_empty());
let input = Nibbles::unpack(input);
prop_assert!(input.iter().all(|&nibble| nibble <= 0xf));
let input_is_odd = input.len() % 2 == 1;

let compact_leaf = input.encode_path_leaf(true);
let leaf_flag = compact_leaf[0];
// Check flag
assert_ne!(leaf_flag & LeafNode::EVEN_FLAG, 0);
assert_eq!(input_is_odd, (leaf_flag & ExtensionNode::ODD_FLAG) != 0);
if input_is_odd {
assert_eq!(leaf_flag & 0x0f, input.first().unwrap());
}

let compact_extension = input.encode_path_leaf(false);
let extension_flag = compact_extension[0];
// Check first byte
assert_eq!(extension_flag & LeafNode::EVEN_FLAG, 0);
assert_eq!(input_is_odd, (extension_flag & ExtensionNode::ODD_FLAG) != 0);
if input_is_odd {
assert_eq!(extension_flag & 0x0f, input.first().unwrap());
}
});
}
}
Loading