Skip to content

Commit 42557ab

Browse files
committed
perf: Optimize CoSMeTIC WASM crate and add benchmarks
Optimizations: - hasher: LUT-based hex encoding (zero-alloc), nibble decoder for from_hex, #[inline(always)] on hot hash functions - tree: batch insert/remove operations, pre-allocated capacity constructor, byte-level cache key masking, snapshot restore, memory usage statistics - proof: shared walk_to_root function eliminating duplicated verification logic, compact proof decompression/roundtrip, compact proof direct verification, short-circuit batch validators, compression ratio reporting - wasm_api: batch insert endpoint for JS, memory stats endpoint, capacity-based constructor New features: - Criterion benchmark suite covering insert, batch insert, proof generation, proof verification, compact proof ops, hex encoding, hash functions, attestation, and snapshot restore - wasm32-unknown-unknown build verified (debug + release) - Test count increased from 35 to 47 https://claude.ai/code/session_01LcbbUBDm1oV2CdeAk3UtTB
1 parent 5502a53 commit 42557ab

8 files changed

Lines changed: 717 additions & 60 deletions

File tree

Cargo.lock

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

examples/experiments/cosmetic-wasm/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ console_error_panic_hook = { version = "0.1", optional = true }
3939

4040
[dev-dependencies]
4141
wasm-bindgen-test = "0.3"
42+
criterion = { version = "0.5", features = ["html_reports"] }
43+
44+
[[bench]]
45+
name = "smt_benchmarks"
46+
harness = false
4247

4348
[profile.release]
4449
lto = true
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
2+
use cosmetic_wasm::hasher::{self, Hash};
3+
use cosmetic_wasm::proof::{self, CompactProof};
4+
use cosmetic_wasm::tree::SparseMerkleTree;
5+
use cosmetic_wasm::attestation::AttestationBuilder;
6+
7+
fn bench_insert(c: &mut Criterion) {
8+
let mut group = c.benchmark_group("smt_insert");
9+
10+
for count in [1, 10, 100] {
11+
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, &n| {
12+
b.iter(|| {
13+
let mut tree = SparseMerkleTree::new();
14+
for i in 0..n {
15+
let key = hasher::compute_key(format!("key_{}", i).as_bytes());
16+
tree.insert(key, b"value".to_vec(), None);
17+
}
18+
black_box(tree.root())
19+
});
20+
});
21+
}
22+
23+
group.finish();
24+
}
25+
26+
fn bench_batch_insert(c: &mut Criterion) {
27+
let mut group = c.benchmark_group("smt_batch_insert");
28+
29+
for count in [10, 50, 100] {
30+
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, &n| {
31+
let entries: Vec<(Hash, Vec<u8>, Option<String>)> = (0..n)
32+
.map(|i| {
33+
let key = hasher::compute_key(format!("batch_{}", i).as_bytes());
34+
(key, format!("value_{}", i).into_bytes(), None)
35+
})
36+
.collect();
37+
38+
b.iter(|| {
39+
let mut tree = SparseMerkleTree::new();
40+
tree.insert_batch(entries.clone());
41+
black_box(tree.root())
42+
});
43+
});
44+
}
45+
46+
group.finish();
47+
}
48+
49+
fn bench_prove_inclusion(c: &mut Criterion) {
50+
let mut tree = SparseMerkleTree::new();
51+
let keys: Vec<Hash> = (0..100)
52+
.map(|i| {
53+
let key = hasher::compute_key(format!("prove_{}", i).as_bytes());
54+
tree.insert(key, b"data".to_vec(), None);
55+
key
56+
})
57+
.collect();
58+
59+
c.bench_function("prove_inclusion", |b| {
60+
let mut idx = 0;
61+
b.iter(|| {
62+
let proof = tree.prove_inclusion(&keys[idx % keys.len()]).unwrap();
63+
idx += 1;
64+
black_box(proof)
65+
});
66+
});
67+
}
68+
69+
fn bench_prove_exclusion(c: &mut Criterion) {
70+
let mut tree = SparseMerkleTree::new();
71+
for i in 0..100 {
72+
let key = hasher::compute_key(format!("exist_{}", i).as_bytes());
73+
tree.insert(key, b"data".to_vec(), None);
74+
}
75+
76+
let absent_keys: Vec<Hash> = (0..100)
77+
.map(|i| hasher::compute_key(format!("absent_{}", i).as_bytes()))
78+
.collect();
79+
80+
c.bench_function("prove_exclusion", |b| {
81+
let mut idx = 0;
82+
b.iter(|| {
83+
let proof = tree.prove_exclusion(&absent_keys[idx % absent_keys.len()]).unwrap();
84+
idx += 1;
85+
black_box(proof)
86+
});
87+
});
88+
}
89+
90+
fn bench_verify_inclusion(c: &mut Criterion) {
91+
let mut tree = SparseMerkleTree::new();
92+
let key = hasher::compute_key(b"verify_bench");
93+
tree.insert(key, b"data".to_vec(), None);
94+
let inc_proof = tree.prove_inclusion(&key).unwrap();
95+
96+
c.bench_function("verify_inclusion", |b| {
97+
b.iter(|| black_box(proof::verify_inclusion(&inc_proof)))
98+
});
99+
}
100+
101+
fn bench_verify_exclusion(c: &mut Criterion) {
102+
let mut tree = SparseMerkleTree::new();
103+
let key = hasher::compute_key(b"present_key");
104+
tree.insert(key, b"data".to_vec(), None);
105+
106+
let absent = hasher::compute_key(b"absent_key");
107+
let exc_proof = tree.prove_exclusion(&absent).unwrap();
108+
109+
c.bench_function("verify_exclusion", |b| {
110+
b.iter(|| black_box(proof::verify_exclusion(&exc_proof)))
111+
});
112+
}
113+
114+
fn bench_compact_proof(c: &mut Criterion) {
115+
let mut tree = SparseMerkleTree::new();
116+
let key = hasher::compute_key(b"compact_bench");
117+
tree.insert(key, b"data".to_vec(), None);
118+
let inc_proof = tree.prove_inclusion(&key).unwrap();
119+
120+
let mut group = c.benchmark_group("compact_proof");
121+
122+
group.bench_function("compress", |b| {
123+
b.iter(|| black_box(CompactProof::from_inclusion(&inc_proof)))
124+
});
125+
126+
let compact = CompactProof::from_inclusion(&inc_proof);
127+
group.bench_function("decompress", |b| {
128+
b.iter(|| black_box(compact.decompress_siblings()))
129+
});
130+
131+
group.bench_function("verify", |b| {
132+
b.iter(|| black_box(compact.verify()))
133+
});
134+
135+
group.finish();
136+
}
137+
138+
fn bench_hex_encoding(c: &mut Criterion) {
139+
let hash = hasher::sha256(b"benchmark");
140+
141+
let mut group = c.benchmark_group("hex");
142+
group.bench_function("to_hex", |b| {
143+
b.iter(|| black_box(hasher::to_hex(&hash)))
144+
});
145+
146+
let hex_str = hasher::to_hex(&hash);
147+
group.bench_function("from_hex", |b| {
148+
b.iter(|| black_box(hasher::from_hex(&hex_str)))
149+
});
150+
group.finish();
151+
}
152+
153+
fn bench_hash_functions(c: &mut Criterion) {
154+
let data = b"benchmark_data_for_hashing_performance";
155+
let key = hasher::sha256(b"key");
156+
let left = hasher::sha256(b"left");
157+
let right = hasher::sha256(b"right");
158+
159+
let mut group = c.benchmark_group("hash");
160+
group.bench_function("sha256", |b| {
161+
b.iter(|| black_box(hasher::sha256(data)))
162+
});
163+
group.bench_function("hash_leaf", |b| {
164+
b.iter(|| black_box(hasher::hash_leaf(&key, data)))
165+
});
166+
group.bench_function("hash_internal", |b| {
167+
b.iter(|| black_box(hasher::hash_internal(&left, &right)))
168+
});
169+
group.bench_function("hash_attestation", |b| {
170+
b.iter(|| black_box(hasher::hash_attestation(&left, &right, b"fn_id", b"params")))
171+
});
172+
group.finish();
173+
}
174+
175+
fn bench_attestation(c: &mut Criterion) {
176+
let input_root = hasher::sha256(b"input");
177+
let output_root = hasher::sha256(b"output");
178+
179+
c.bench_function("attestation_build_verify", |b| {
180+
b.iter(|| {
181+
let key1 = hasher::compute_key(b"p1");
182+
let key2 = hasher::compute_key(b"p2");
183+
let att = AttestationBuilder::new("filter")
184+
.with_parameters(b"age>=18".to_vec())
185+
.include(key1, "eligible", "age>=18")
186+
.exclude(key2, "too young", "age>=18")
187+
.build(input_root, output_root);
188+
black_box(cosmetic_wasm::attestation::verify_attestation(&att))
189+
});
190+
});
191+
}
192+
193+
fn bench_snapshot_restore(c: &mut Criterion) {
194+
let mut tree = SparseMerkleTree::new();
195+
for i in 0..50 {
196+
let key = hasher::compute_key(format!("snap_{}", i).as_bytes());
197+
tree.insert(key, format!("val_{}", i).into_bytes(), None);
198+
}
199+
200+
c.bench_function("snapshot_restore_50", |b| {
201+
let snap = tree.snapshot();
202+
b.iter(|| {
203+
let restored = SparseMerkleTree::from_snapshot(snap.clone());
204+
black_box(restored.root())
205+
});
206+
});
207+
}
208+
209+
criterion_group!(
210+
benches,
211+
bench_insert,
212+
bench_batch_insert,
213+
bench_prove_inclusion,
214+
bench_prove_exclusion,
215+
bench_verify_inclusion,
216+
bench_verify_exclusion,
217+
bench_compact_proof,
218+
bench_hex_encoding,
219+
bench_hash_functions,
220+
bench_attestation,
221+
bench_snapshot_restore,
222+
);
223+
criterion_main!(benches);

examples/experiments/cosmetic-wasm/src/hasher.rs

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ pub type Hash = [u8; HASH_SIZE];
1616
/// H("cosmetic_empty_leaf")
1717
pub const DEFAULT_EMPTY: Hash = {
1818
// Pre-computed SHA-256 of b"cosmetic_empty_leaf"
19-
// We use a const fn workaround: store the known digest bytes directly.
2019
[
2120
0x8b, 0x1a, 0x9d, 0x7f, 0x3e, 0x2c, 0x5b, 0x4a, 0x91, 0x0e, 0xd3, 0xf8, 0x76, 0xc5,
2221
0xa4, 0x23, 0x6d, 0x1f, 0x8e, 0xb7, 0x50, 0x9c, 0xe2, 0x34, 0xa8, 0x67, 0xdb, 0x19,
2322
0xf0, 0x5c, 0x83, 0xe1,
2423
]
2524
};
2625

26+
/// Hex lookup table for fast encoding (avoids per-byte format! allocation)
27+
const HEX_LUT: &[u8; 16] = b"0123456789abcdef";
28+
2729
/// Compute SHA-256 hash of arbitrary data
28-
#[inline]
30+
#[inline(always)]
2931
pub fn sha256(data: &[u8]) -> Hash {
3032
let mut hasher = Sha256::new();
3133
hasher.update(data);
@@ -34,7 +36,7 @@ pub fn sha256(data: &[u8]) -> Hash {
3436

3537
/// Compute the hash of two child nodes (internal node hash)
3638
/// H(left || right)
37-
#[inline]
39+
#[inline(always)]
3840
pub fn hash_pair(left: &Hash, right: &Hash) -> Hash {
3941
let mut hasher = Sha256::new();
4042
hasher.update(left);
@@ -45,7 +47,7 @@ pub fn hash_pair(left: &Hash, right: &Hash) -> Hash {
4547
/// Compute the hash of a leaf node
4648
/// H(0x00 || key || value)
4749
/// The 0x00 prefix domain-separates leaf hashes from internal node hashes.
48-
#[inline]
50+
#[inline(always)]
4951
pub fn hash_leaf(key: &Hash, value: &[u8]) -> Hash {
5052
let mut hasher = Sha256::new();
5153
hasher.update([0x00]); // leaf domain separator
@@ -57,7 +59,7 @@ pub fn hash_leaf(key: &Hash, value: &[u8]) -> Hash {
5759
/// Compute the hash of an internal node
5860
/// H(0x01 || left || right)
5961
/// The 0x01 prefix domain-separates internal hashes from leaf hashes.
60-
#[inline]
62+
#[inline(always)]
6163
pub fn hash_internal(left: &Hash, right: &Hash) -> Hash {
6264
let mut hasher = Sha256::new();
6365
hasher.update([0x01]); // internal domain separator
@@ -87,12 +89,16 @@ pub fn hash_attestation(
8789
/// Simple algebraic hash for ZK-friendly operations (Poseidon-style placeholder).
8890
///
8991
/// In a real ZK deployment, this would be replaced with a proper Poseidon
90-
/// permutation over a prime field. This simplified version provides the
91-
/// same API shape for testing and WASM demonstration purposes.
92+
/// permutation over a prime field (e.g., BN254 scalar field). This simplified
93+
/// version uses domain-separated SHA-256 to provide the same API shape.
94+
///
95+
/// A real Poseidon implementation would:
96+
/// - Operate over Fp elements (field arithmetic, not byte operations)
97+
/// - Use ~8x fewer constraints in R1CS/Plonkish circuits
98+
/// - Provide algebraic collision resistance (not just generic)
99+
/// - Support variable-width inputs via a sponge construction
92100
#[cfg(feature = "poseidon")]
93101
pub fn poseidon_hash(inputs: &[u64]) -> Hash {
94-
// Simplified: feed u64 inputs through SHA-256 as a stand-in
95-
// A real Poseidon would operate over Fp elements with ~8x fewer constraints
96102
let mut hasher = Sha256::new();
97103
hasher.update([0x03]); // poseidon domain separator
98104
for val in inputs {
@@ -101,25 +107,47 @@ pub fn poseidon_hash(inputs: &[u64]) -> Hash {
101107
hasher.finalize().into()
102108
}
103109

104-
/// Convert a hash to a hex string
110+
/// Convert a hash to a hex string (zero-allocation fast path)
111+
#[inline]
105112
pub fn to_hex(hash: &Hash) -> String {
106-
hash.iter().map(|b| format!("{:02x}", b)).collect()
113+
let mut hex = Vec::with_capacity(HASH_SIZE * 2);
114+
for &b in hash {
115+
hex.push(HEX_LUT[(b >> 4) as usize]);
116+
hex.push(HEX_LUT[(b & 0x0f) as usize]);
117+
}
118+
// SAFETY: HEX_LUT only contains valid ASCII bytes
119+
unsafe { String::from_utf8_unchecked(hex) }
107120
}
108121

109122
/// Parse a hex string into a Hash
123+
#[inline]
110124
pub fn from_hex(hex: &str) -> Result<Hash, &'static str> {
111125
if hex.len() != HASH_SIZE * 2 {
112126
return Err("Invalid hex length");
113127
}
128+
let bytes = hex.as_bytes();
114129
let mut hash = [0u8; HASH_SIZE];
115130
for i in 0..HASH_SIZE {
116-
hash[i] =
117-
u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| "Invalid hex digit")?;
131+
let hi = hex_nibble(bytes[i * 2])?;
132+
let lo = hex_nibble(bytes[i * 2 + 1])?;
133+
hash[i] = (hi << 4) | lo;
118134
}
119135
Ok(hash)
120136
}
121137

138+
/// Decode a single hex character to its nibble value
139+
#[inline(always)]
140+
fn hex_nibble(c: u8) -> Result<u8, &'static str> {
141+
match c {
142+
b'0'..=b'9' => Ok(c - b'0'),
143+
b'a'..=b'f' => Ok(c - b'a' + 10),
144+
b'A'..=b'F' => Ok(c - b'A' + 10),
145+
_ => Err("Invalid hex digit"),
146+
}
147+
}
148+
122149
/// Compute the key (address) for a given data blob by hashing it
150+
#[inline]
123151
pub fn compute_key(data: &[u8]) -> Hash {
124152
sha256(data)
125153
}
@@ -164,4 +192,13 @@ mod tests {
164192
assert!(from_hex("not_valid_hex").is_err());
165193
assert!(from_hex("abcd").is_err()); // too short
166194
}
195+
196+
#[test]
197+
fn test_hex_case_insensitive() {
198+
let hash = sha256(b"test");
199+
let hex_lower = to_hex(&hash);
200+
let hex_upper = hex_lower.to_uppercase();
201+
let recovered = from_hex(&hex_upper).unwrap();
202+
assert_eq!(hash, recovered);
203+
}
167204
}

examples/experiments/cosmetic-wasm/src/lib.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,13 @@ pub mod wasm_api;
7474

7575
pub use attestation::{AttestationBuilder, AttestationLog, ComputationAttestation};
7676
pub use hasher::{Hash, HASH_SIZE};
77-
pub use proof::{verify_exclusion, verify_inclusion, CompactProof, VerifyResult};
78-
pub use tree::{ExclusionProof, InclusionProof, SparseMerkleTree};
77+
pub use proof::{
78+
all_valid_exclusion, all_valid_inclusion, verify_exclusion, verify_inclusion, CompactProof,
79+
VerifyResult,
80+
};
81+
pub use tree::{
82+
BatchMutationResult, ExclusionProof, InclusionProof, SparseMerkleTree, TreeMemoryStats,
83+
};
7984
pub use wasm_api::CosmeticTree;
8085

8186
/// Initialize panic hook for better error messages in browser console

0 commit comments

Comments
 (0)