Skip to main content

ant_node/replication/
commitment.rs

1//! Storage-bound audit via piggybacked commitments.
2//!
3//! Implements the v12 storage-bound audit design: it closes the
4//! storage-binding holes where a node could pass audits while holding chunk
5//! addresses (not bytes), or answer against a commitment it never gossiped.
6//!
7//! ## What this module provides
8//!
9//! - [`StorageCommitment`] — the wire type sent on neighbour-sync gossip
10//!   and embedded in commitment-bound audit responses. `ML-DSA-65` signed
11//!   over `(root, key_count, sender_peer_id)` with explicit domain separation.
12//! - [`MerkleTree`] — an in-memory Merkle tree over `(key, BLAKE3(bytes))`
13//!   leaves. Rebuilt by the responder when its key set changes; produces
14//!   inclusion paths used in audit responses.
15//! - [`commitment_hash`] — the auditor's pin: a `BLAKE3` digest over the
16//!   full signed commitment blob. Audit challenges carry this; audit
17//!   responses must include a commitment that hashes to the same value.
18//! - [`verify_path`] — auditor's per-key check: rebuilds the leaf from
19//!   `(key, bytes_hash)` and verifies the inclusion path against the
20//!   committed root.
21//!
22//! Nothing else (responder gossip loop, auditor verify path,
23//! reward-eligibility cache) lives here yet — that's the next phase.
24
25use blake3::Hasher;
26use saorsa_pqc::api::sig::{
27    ml_dsa_65, MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature, MlDsaVariant,
28};
29use serde::{Deserialize, Serialize};
30
31use crate::ant_protocol::XorName;
32
33/// Domain-separation tag for the commitment signature.
34///
35/// Signed payload is BLAKE3 over (this tag || canonical commitment fields).
36pub const DOMAIN_COMMITMENT: &[u8] = b"autonomi.ant.replication.storage_commitment.v1";
37
38/// Domain-separation tag for the auditor's pin: BLAKE3 over (this tag ||
39/// canonical commitment blob).
40pub const DOMAIN_COMMITMENT_HASH: &[u8] = b"autonomi.ant.replication.commitment_hash.v1";
41
42/// Domain-separation tag for Merkle leaves: `BLAKE3(this || key || H(bytes))`.
43pub const DOMAIN_LEAF: &[u8] = b"autonomi.ant.replication.storage_leaf.v1";
44
45/// Domain-separation tag for Merkle internal nodes: `BLAKE3(this || left || right)`.
46pub const DOMAIN_NODE: &[u8] = b"autonomi.ant.replication.storage_node.v1";
47
48/// Maximum number of keys a single commitment may cover.
49///
50/// Bounds the Merkle path depth (audit responses carry `O(log2 key_count)`
51/// hashes per key) and the responder-side tree memory. A node storing more
52/// keys than this would need to split its claim — out of scope for v1.
53pub const MAX_COMMITMENT_KEY_COUNT: u32 = 1_000_000;
54
55/// Signed storage commitment.
56///
57/// Piggybacked on neighbour-sync gossip. The signature commits to the
58/// Merkle root, key count, sender peer ID, **and the sender's ML-DSA-65
59/// public key** under [`DOMAIN_COMMITMENT`].
60///
61/// Embedding the public key lets any receiver verify the signature
62/// without an external `PeerId → MlDsaPublicKey` lookup. Binding the
63/// public key in the signed payload prevents a key-swap attack where an
64/// adversary keeps the message body but re-signs it under a different key
65/// to claim a different identity. The peer-id binding (gate 2a in
66/// `verify_commitment_bound_response`) still ensures the embedded key
67/// belongs to the gossiping peer.
68///
69/// # Wire size
70///
71/// One commitment is approximately 5.3 KiB:
72///   - root: 32 B
73///   - `key_count`: 4 B
74///   - `sender_peer_id`: 32 B
75///   - `sender_public_key`: 1952 B (ML-DSA-65 public key)
76///   - signature: 3293 B (ML-DSA-65 signature)
77///
78/// Piggybacked on every `NeighborSyncRequest`/`Response` (~1 h interval
79/// per close-group peer at the neighbour-sync cooldown cadence). At a
80/// realistic close-group size of 8 with bidirectional sync, that's
81/// roughly 8 × 2 × 5.3 KiB / hour = ~85 KiB/h of additional gossip
82/// per node. Negligible against typical chunk-transfer bandwidth.
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
84pub struct StorageCommitment {
85    /// Merkle root over the responder's claimed keys.
86    pub root: [u8; 32],
87    /// Number of leaves committed over.
88    pub key_count: u32,
89    /// Sender peer ID, bound to the signature.
90    pub sender_peer_id: [u8; 32],
91    /// Sender's ML-DSA-65 public key bytes (1952 bytes). Embedded so
92    /// receivers can verify the signature without a separate pubkey
93    /// directory. Bound by the signature.
94    pub sender_public_key: Vec<u8>,
95    /// ML-DSA-65 signature over canonical commitment fields. 3293 bytes.
96    pub signature: Vec<u8>,
97}
98
99// ---------------------------------------------------------------------------
100// Hashing helpers
101// ---------------------------------------------------------------------------
102
103/// Compute the Merkle leaf hash for `(key, bytes_hash)`.
104///
105/// `bytes_hash` is BLAKE3 over the record bytes; the leaf binds the key to
106/// the content so an adversary cannot reuse a leaf for a different chunk.
107#[must_use]
108pub fn leaf_hash(key: &XorName, bytes_hash: &[u8; 32]) -> [u8; 32] {
109    let mut h = Hasher::new();
110    h.update(DOMAIN_LEAF);
111    h.update(key);
112    h.update(bytes_hash);
113    *h.finalize().as_bytes()
114}
115
116/// Combine two child hashes into a Merkle internal-node hash.
117#[must_use]
118pub fn node_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
119    let mut h = Hasher::new();
120    h.update(DOMAIN_NODE);
121    h.update(left);
122    h.update(right);
123    *h.finalize().as_bytes()
124}
125
126/// The auditor's pin: `BLAKE3(DOMAIN_COMMITMENT_HASH || postcard(commitment))`.
127///
128/// Equal commitments produce equal hashes; any change to `root`, `key_count`,
129/// peer ID, or signature changes the hash because postcard's canonical
130/// encoding includes a length prefix for `signature`. The audit challenge
131/// carries this value; the audit response must include a commitment that
132/// hashes to the same value, defeating fresh-commitment substitution.
133///
134/// Postcard encoding is the same canonical wire form the rest of the
135/// replication protocol uses (`MessageCodec::encode`), so an encoded
136/// commitment from a `NeighborSyncRequest` produces the same hash as the
137/// same commitment received in an `AuditResponse`.
138///
139/// # Errors
140///
141/// Returns `None` only if postcard fails to serialize the commitment, which
142/// in practice means the signature is somehow `> isize::MAX` bytes — not
143/// reachable for ML-DSA-65 (3293 bytes). Callers may safely treat `None` as
144/// a malformed commitment and drop it.
145#[must_use]
146pub fn commitment_hash(c: &StorageCommitment) -> Option<[u8; 32]> {
147    let serialized = postcard::to_allocvec(c).ok()?;
148    let mut h = Hasher::new();
149    h.update(DOMAIN_COMMITMENT_HASH);
150    h.update(&serialized);
151    Some(*h.finalize().as_bytes())
152}
153
154/// Canonical bytes the ML-DSA signature covers: the commitment fields
155/// minus the signature itself.
156///
157/// `sender_public_key` is included so an adversary cannot keep the body
158/// and re-sign under a different key (the audit-time verifier would
159/// otherwise accept the swap because verification uses the embedded key).
160fn commitment_signed_payload(
161    root: &[u8; 32],
162    key_count: u32,
163    sender_peer_id: &[u8; 32],
164    sender_public_key: &[u8],
165) -> Vec<u8> {
166    let mut v = Vec::with_capacity(32 + 4 + 32 + 4 + sender_public_key.len());
167    v.extend_from_slice(root);
168    v.extend_from_slice(&key_count.to_le_bytes());
169    v.extend_from_slice(sender_peer_id);
170    // Length-prefix the pubkey so two different (key, suffix) splits cannot
171    // produce the same byte stream (canonical encoding).
172    let pk_len = u32::try_from(sender_public_key.len()).unwrap_or(u32::MAX);
173    v.extend_from_slice(&pk_len.to_le_bytes());
174    v.extend_from_slice(sender_public_key);
175    v
176}
177
178// ---------------------------------------------------------------------------
179// Merkle tree
180// ---------------------------------------------------------------------------
181
182/// In-memory Merkle tree over the responder's claimed keys.
183///
184/// Leaves are `BLAKE3(DOMAIN_LEAF || key || BLAKE3(bytes))`, sorted by
185/// `key`. Internal nodes are `BLAKE3(DOMAIN_NODE || left || right)`. When
186/// a level has an odd number of nodes, the last node is paired with
187/// **itself** — i.e. `node_hash(x, x)` — so the level above has
188/// `ceil(n/2)` nodes. This is a standard self-pair construction (NOT
189/// node promotion) and deterministically maps any non-empty key set to
190/// a single root.
191///
192/// Rebuilt by the responder whenever its key set changes meaningfully
193/// (debounced in the integration layer; not this module's concern).
194pub struct MerkleTree {
195    /// Sorted leaves, indexed by their position in the sorted key set.
196    ///
197    /// `leaves[i] = (key_i, leaf_hash(key_i, bytes_hash_i))`.
198    leaves: Vec<(XorName, [u8; 32])>,
199    /// Tree levels, level 0 is the leaves and the last level is the root.
200    ///
201    /// `levels[0].len() == leaves.len()`; `levels[L].len() == 1` where L
202    /// is the root level.
203    levels: Vec<Vec<[u8; 32]>>,
204}
205
206impl MerkleTree {
207    /// Build a Merkle tree over `(key, bytes_hash)` pairs.
208    ///
209    /// `entries` does not need to be sorted; this method sorts internally
210    /// so the produced root is deterministic per key set. Duplicate keys
211    /// are an error: the responder must deduplicate before calling.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if `entries` is empty (no commitment to make), if
216    /// `entries.len() > MAX_COMMITMENT_KEY_COUNT`, or if it contains
217    /// duplicate keys.
218    pub fn build(mut entries: Vec<(XorName, [u8; 32])>) -> Result<Self, CommitmentError> {
219        if entries.is_empty() {
220            return Err(CommitmentError::EmptyKeySet);
221        }
222        if entries.len() > MAX_COMMITMENT_KEY_COUNT as usize {
223            return Err(CommitmentError::TooManyKeys(entries.len()));
224        }
225
226        entries.sort_by_key(|a| a.0);
227        for w in entries.windows(2) {
228            if let [a, b] = w {
229                if a.0 == b.0 {
230                    return Err(CommitmentError::DuplicateKey(a.0));
231                }
232            }
233        }
234
235        let leaves: Vec<(XorName, [u8; 32])> = entries
236            .into_iter()
237            .map(|(k, bh)| {
238                let lh = leaf_hash(&k, &bh);
239                (k, lh)
240            })
241            .collect();
242
243        let mut level: Vec<[u8; 32]> = leaves.iter().map(|(_, h)| *h).collect();
244        let mut levels = vec![level.clone()];
245        while level.len() > 1 {
246            level = build_next_level(&level);
247            levels.push(level.clone());
248        }
249
250        Ok(Self { leaves, levels })
251    }
252
253    /// The Merkle root of this tree.
254    ///
255    /// `unwrap`-free: `build` guarantees at least one level with at least
256    /// one entry, so `last().first()` is always `Some`.
257    #[must_use]
258    pub fn root(&self) -> [u8; 32] {
259        // SAFETY: build() enforces non-empty entries → non-empty leaves →
260        // non-empty levels → last level has exactly one hash.
261        self.levels
262            .last()
263            .and_then(|l| l.first())
264            .copied()
265            .unwrap_or([0u8; 32])
266    }
267
268    /// The number of leaves (== claimed keys).
269    #[must_use]
270    pub fn key_count(&self) -> u32 {
271        // Cast is safe because build() rejects > MAX_COMMITMENT_KEY_COUNT.
272        u32::try_from(self.leaves.len()).unwrap_or(u32::MAX)
273    }
274
275    /// Inclusion path for `key` from its leaf up to (but not including)
276    /// the root.
277    ///
278    /// Returns `None` if `key` is not in this tree.
279    #[must_use]
280    pub fn path_for(&self, key: &XorName) -> Option<Vec<[u8; 32]>> {
281        let idx = self.leaves.binary_search_by(|(k, _)| k.cmp(key)).ok()?;
282
283        let mut path = Vec::with_capacity(self.levels.len());
284        let mut i = idx;
285        for level in &self.levels[..self.levels.len().saturating_sub(1)] {
286            // Sibling is the *other* half of the pair containing `i`. If
287            // `i` is the unpaired last node at this level, its sibling is
288            // itself (matches the self-pair construction in
289            // `build_next_level`).
290            let sibling_idx = if i % 2 == 0 {
291                if i + 1 < level.len() {
292                    i + 1
293                } else {
294                    i
295                }
296            } else {
297                i - 1
298            };
299            path.push(level[sibling_idx]);
300            i /= 2;
301        }
302        Some(path)
303    }
304
305    /// Iterate over `(key, leaf_hash)` pairs in sorted order. Test-only.
306    #[cfg(test)]
307    pub(crate) fn iter_leaves(&self) -> impl Iterator<Item = &(XorName, [u8; 32])> {
308        self.leaves.iter()
309    }
310
311    /// The keys this tree commits to, in sorted order.
312    ///
313    /// `sorted_keys()[i]` is the key at leaf index `i`. Used by the
314    /// responder's audit-answer path to recover the `leaf_index` field
315    /// for a challenged key in `O(log n)` via binary search.
316    #[must_use]
317    pub fn sorted_keys(&self) -> Vec<XorName> {
318        self.leaves.iter().map(|(k, _)| *k).collect()
319    }
320
321    /// The key at sorted leaf index `idx`, if in range.
322    ///
323    /// Used by the subtree-proof builder to enumerate the keys of a
324    /// contiguous leaf range without cloning the whole key list.
325    #[must_use]
326    pub fn key_at(&self, idx: usize) -> Option<XorName> {
327        self.leaves.get(idx).map(|(k, _)| *k)
328    }
329
330    /// The sorted leaf index of `key`, if committed. `O(log n)` binary search
331    /// over the (key-sorted) leaves — no separate key list needed, so callers
332    /// don't have to keep a duplicate `sorted_keys` Vec alongside the tree.
333    #[must_use]
334    pub fn key_index(&self, key: &XorName) -> Option<usize> {
335        self.leaves.binary_search_by(|(k, _)| k.cmp(key)).ok()
336    }
337
338    /// Whether `key` is committed. Allocation-free membership check via the same
339    /// binary search as [`Self::key_index`].
340    #[must_use]
341    pub fn contains_key(&self, key: &XorName) -> bool {
342        self.key_index(key).is_some()
343    }
344
345    /// The node hash at `(level, index)`, where `level` counts up from the
346    /// leaves (`level == 0` is the leaf level, the last level is the root).
347    ///
348    /// Returns `None` if out of range. Used by the subtree-proof builder to
349    /// read sibling cut-hashes along the path from the root to the selected
350    /// subtree; honours the same left-packed self-pair construction as the
351    /// rest of the tree (a caller asking for an out-of-range sibling on an
352    /// odd-length level should substitute the node itself).
353    #[must_use]
354    pub fn node_at(&self, level: usize, index: u64) -> Option<[u8; 32]> {
355        let index = usize::try_from(index).ok()?;
356        self.levels.get(level).and_then(|l| l.get(index)).copied()
357    }
358
359    /// The number of levels in the tree (`1` for a single-leaf tree; the
360    /// last index is the root level). `depth == levels_count() - 1`.
361    #[must_use]
362    pub fn levels_count(&self) -> usize {
363        self.levels.len()
364    }
365}
366
367/// Build the next level up from `cur`. Odd-length levels pair the last
368/// node with itself (`node_hash(x, x)`) so the level above has
369/// `ceil(n/2)` nodes. Keeps the tree balanced without needing a dummy
370/// leaf domain.
371///
372/// `pub(crate)` so the subtree-proof verifier folds a contiguous leaf block to
373/// its subtree root with the EXACT same self-pair rule (§10 — previously
374/// duplicated as `fold_levels`'s inner loop), guaranteeing the rebuilt node
375/// matches the committed tree bit-for-bit.
376pub(crate) fn build_next_level(cur: &[[u8; 32]]) -> Vec<[u8; 32]> {
377    let mut next = Vec::with_capacity(cur.len().div_ceil(2));
378    let mut i = 0;
379    while i < cur.len() {
380        let left = &cur[i];
381        let right = if i + 1 < cur.len() { &cur[i + 1] } else { left };
382        next.push(node_hash(left, right));
383        i += 2;
384    }
385    next
386}
387
388/// Verify an inclusion path against a commitment of size `key_count`.
389///
390/// `leaf_index` is the responder's position of this leaf in the sorted
391/// leaf set; the commitment's `key_count` comes from
392/// `StorageCommitment.key_count`.
393/// At each level of the path, if the current index is even, the current
394/// hash is the left child and we compute `node_hash(self, sibling)`;
395/// otherwise it is the right child and we compute `node_hash(sibling, self)`.
396///
397/// Returns `true` iff:
398///   - `leaf_index < key_count` (rejects out-of-range claims), AND
399///   - `path.len() == ceil(log2(key_count))` for `key_count > 1`, or
400///     `path.is_empty()` for `key_count == 1` (rejects wrong-shape paths
401///     before doing any hashing), AND
402///   - the recomputed root equals `expected_root`.
403#[must_use]
404pub fn verify_path(
405    leaf: &[u8; 32],
406    path: &[[u8; 32]],
407    leaf_index: usize,
408    key_count: u32,
409    expected_root: &[u8; 32],
410) -> bool {
411    if key_count == 0
412        || key_count > MAX_COMMITMENT_KEY_COUNT
413        || (leaf_index as u64) >= u64::from(key_count)
414    {
415        return false;
416    }
417    // Tree depth = ceil(log2(key_count)). For a power-of-two `n`,
418    // `n.next_power_of_two() == n` so trailing_zeros == log2(n). For non
419    // powers-of-two, next_power_of_two rounds up so trailing_zeros gives
420    // ceil(log2). Special case: key_count == 1 → next_power_of_two == 1
421    // → trailing_zeros == 0 → empty path, which matches the single-leaf
422    // tree's root == leaf invariant.
423    //
424    // `checked_next_power_of_two` returns None on overflow; combined with
425    // the MAX_COMMITMENT_KEY_COUNT cap above it cannot fail in practice,
426    // but the explicit check is profile-independent (release vs debug
427    // would otherwise differ on overflow per Rust's primitive docs).
428    let Some(rounded) = key_count.checked_next_power_of_two() else {
429        return false;
430    };
431    let expected_path_len = rounded.trailing_zeros() as usize;
432    if path.len() != expected_path_len {
433        return false;
434    }
435
436    let mut cur = *leaf;
437    let mut i = leaf_index;
438    for sibling in path {
439        cur = if i % 2 == 0 {
440            node_hash(&cur, sibling)
441        } else {
442            node_hash(sibling, &cur)
443        };
444        i /= 2;
445    }
446    cur == *expected_root
447}
448
449// ---------------------------------------------------------------------------
450// Sign + verify
451// ---------------------------------------------------------------------------
452
453/// Sign a commitment's `(root, key_count, sender_peer_id, sender_public_key)`
454/// with `secret_key`.
455///
456/// The signature is over the canonical signed payload (see
457/// `commitment_signed_payload`) under [`DOMAIN_COMMITMENT`].
458///
459/// # Errors
460///
461/// Returns an error if the underlying ML-DSA-65 signer fails.
462pub fn sign_commitment(
463    secret_key: &MlDsaSecretKey,
464    root: &[u8; 32],
465    key_count: u32,
466    sender_peer_id: &[u8; 32],
467    sender_public_key: &[u8],
468) -> Result<Vec<u8>, CommitmentError> {
469    let payload = commitment_signed_payload(root, key_count, sender_peer_id, sender_public_key);
470    let dsa = ml_dsa_65();
471    let sig = dsa
472        .sign_with_context(secret_key, &payload, DOMAIN_COMMITMENT)
473        .map_err(|e| CommitmentError::SignatureFailed(e.to_string()))?;
474    Ok(sig.to_bytes())
475}
476
477/// Verify a commitment's signature using the embedded `sender_public_key`.
478///
479/// Returns `true` iff the signature is valid for `(root, key_count,
480/// sender_peer_id, sender_public_key)` under `c.sender_public_key` and
481/// [`DOMAIN_COMMITMENT`]. Returns `false` on key-format or signature-format
482/// errors so the caller can simply drop the gossip.
483///
484/// Verifying against the embedded key removes the need for an external
485/// `PeerId → MlDsaPublicKey` lookup. The peer-id binding gate in
486/// `ingest_peer_commitment` (and the auditor's `evaluate_subtree_structure`)
487/// still ensures the embedded key belongs to the claimed peer.
488#[must_use]
489pub fn verify_commitment_signature(c: &StorageCommitment) -> bool {
490    let Ok(public_key) = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, &c.sender_public_key)
491    else {
492        return false;
493    };
494    verify_commitment_signature_with_key(c, &public_key)
495}
496
497/// Verify a commitment's signature against an externally provided key.
498///
499/// Test-helper variant. Production code should use [`verify_commitment_signature`]
500/// since the key is embedded in the commitment.
501#[must_use]
502pub fn verify_commitment_signature_with_key(
503    c: &StorageCommitment,
504    public_key: &MlDsaPublicKey,
505) -> bool {
506    let payload = commitment_signed_payload(
507        &c.root,
508        c.key_count,
509        &c.sender_peer_id,
510        &c.sender_public_key,
511    );
512    let Ok(sig) = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &c.signature) else {
513        return false;
514    };
515    let dsa = ml_dsa_65();
516    dsa.verify_with_context(public_key, &payload, &sig, DOMAIN_COMMITMENT)
517        .unwrap_or(false)
518}
519
520// ---------------------------------------------------------------------------
521// Errors
522// ---------------------------------------------------------------------------
523
524/// Errors from commitment construction or verification.
525#[derive(Debug, Clone, thiserror::Error)]
526pub enum CommitmentError {
527    /// `MerkleTree::build` was called with an empty key set.
528    #[error("cannot build commitment over empty key set")]
529    EmptyKeySet,
530    /// Key set exceeds [`MAX_COMMITMENT_KEY_COUNT`].
531    #[error("commitment key count {0} exceeds MAX_COMMITMENT_KEY_COUNT")]
532    TooManyKeys(usize),
533    /// `MerkleTree::build` received the same key twice.
534    #[error("duplicate key in commitment: {}", hex::encode(.0))]
535    DuplicateKey(XorName),
536    /// Underlying ML-DSA-65 signer failed.
537    #[error("commitment signing failed: {0}")]
538    SignatureFailed(String),
539}
540
541// ---------------------------------------------------------------------------
542// Tests
543// ---------------------------------------------------------------------------
544
545#[cfg(test)]
546#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
547mod tests {
548    use super::*;
549
550    fn xn(byte: u8) -> XorName {
551        [byte; 32]
552    }
553
554    fn bh(byte: u8) -> [u8; 32] {
555        [byte ^ 0x5A; 32]
556    }
557
558    #[test]
559    fn empty_key_set_rejected() {
560        let result = MerkleTree::build(vec![]);
561        assert!(matches!(result, Err(CommitmentError::EmptyKeySet)));
562    }
563
564    #[test]
565    fn duplicate_keys_rejected() {
566        let result = MerkleTree::build(vec![(xn(1), bh(1)), (xn(1), bh(2))]);
567        assert!(matches!(result, Err(CommitmentError::DuplicateKey(_))));
568    }
569
570    #[test]
571    fn single_leaf_tree_root_is_leaf_hash() {
572        let key = xn(1);
573        let bytes_hash = bh(1);
574        let tree = MerkleTree::build(vec![(key, bytes_hash)]).unwrap();
575        assert_eq!(tree.root(), leaf_hash(&key, &bytes_hash));
576        assert_eq!(tree.key_count(), 1);
577        assert_eq!(tree.path_for(&key), Some(vec![]));
578        // Empty path verifies trivially (root == leaf).
579        assert!(verify_path(
580            &leaf_hash(&key, &bytes_hash),
581            &[],
582            0,
583            1,
584            &tree.root()
585        ));
586    }
587
588    #[test]
589    fn two_leaf_tree_root_combines_both_leaves() {
590        let entries = vec![(xn(1), bh(1)), (xn(2), bh(2))];
591        let tree = MerkleTree::build(entries).unwrap();
592        // Sorted order: xn(1), xn(2).
593        let l1 = leaf_hash(&xn(1), &bh(1));
594        let l2 = leaf_hash(&xn(2), &bh(2));
595        assert_eq!(tree.root(), node_hash(&l1, &l2));
596    }
597
598    #[test]
599    fn root_is_deterministic_regardless_of_input_order() {
600        let mut a = vec![(xn(3), bh(3)), (xn(1), bh(1)), (xn(2), bh(2))];
601        let mut b = vec![(xn(2), bh(2)), (xn(3), bh(3)), (xn(1), bh(1))];
602        let tree_a = MerkleTree::build(a.clone()).unwrap();
603        let tree_b = MerkleTree::build(b.clone()).unwrap();
604        a.sort_by_key(|x| x.0);
605        b.sort_by_key(|x| x.0);
606        assert_eq!(tree_a.root(), tree_b.root());
607    }
608
609    fn xn_u32(i: u32) -> XorName {
610        let mut k = [0u8; 32];
611        k[..4].copy_from_slice(&i.to_le_bytes());
612        k
613    }
614
615    fn bh_u32(i: u32) -> [u8; 32] {
616        let mut h = [0u8; 32];
617        h[..4].copy_from_slice(&i.to_le_bytes());
618        h[4] = 0x5A;
619        h
620    }
621
622    #[test]
623    fn paths_verify_for_every_key_at_various_sizes() {
624        for n in [1u32, 2, 3, 4, 5, 7, 8, 16, 17, 100, 333] {
625            let entries: Vec<_> = (0..n).map(|i| (xn_u32(i), bh_u32(i))).collect();
626            let tree = MerkleTree::build(entries.clone()).unwrap();
627            let root = tree.root();
628            let key_count = tree.key_count();
629            for (idx, (k, _)) in tree.iter_leaves().enumerate() {
630                let path = tree.path_for(k).expect("path for present key");
631                let bytes_hash = entries.iter().find(|(kk, _)| kk == k).unwrap().1;
632                let lh = leaf_hash(k, &bytes_hash);
633                assert!(
634                    verify_path(&lh, &path, idx, key_count, &root),
635                    "path verify failed at n={n} idx={idx}",
636                );
637            }
638        }
639    }
640
641    #[test]
642    fn path_for_absent_key_is_none() {
643        let tree = MerkleTree::build(vec![(xn(1), bh(1)), (xn(2), bh(2))]).unwrap();
644        assert!(tree.path_for(&xn(99)).is_none());
645    }
646
647    #[test]
648    fn tampered_bytes_hash_breaks_path_verify() {
649        // Use 8 distinct sorted keys so the index in `entries` matches the
650        // sorted leaf index in the tree.
651        let entries: Vec<_> = (1..=8u8).map(|i| (xn(i), bh(i))).collect();
652        let tree = MerkleTree::build(entries.clone()).unwrap();
653        let root = tree.root();
654        let (k, _) = &entries[3];
655        let path = tree.path_for(k).unwrap();
656
657        let wrong_bytes_hash = [0xFFu8; 32];
658        let lh = leaf_hash(k, &wrong_bytes_hash);
659        assert!(!verify_path(&lh, &path, 3, 8, &root));
660    }
661
662    #[test]
663    fn tampered_path_node_breaks_verify() {
664        let entries: Vec<_> = (1..=8u8).map(|i| (xn(i), bh(i))).collect();
665        let tree = MerkleTree::build(entries.clone()).unwrap();
666        let root = tree.root();
667        let (k, _) = &entries[3];
668        let mut path = tree.path_for(k).unwrap();
669        path[0][0] ^= 0x01;
670        let lh = leaf_hash(k, &bh(4));
671        assert!(!verify_path(&lh, &path, 3, 8, &root));
672    }
673
674    #[test]
675    fn wrong_leaf_index_breaks_verify() {
676        let entries: Vec<_> = (1..=8u8).map(|i| (xn(i), bh(i))).collect();
677        let tree = MerkleTree::build(entries.clone()).unwrap();
678        let root = tree.root();
679        let (k, _) = &entries[3];
680        let path = tree.path_for(k).unwrap();
681        let lh = leaf_hash(k, &bh(4));
682        // Correct index is 3; using 2 should fail because the left/right
683        // child ordering swaps.
684        assert!(!verify_path(&lh, &path, 2, 8, &root));
685        assert!(verify_path(&lh, &path, 3, 8, &root));
686    }
687
688    #[test]
689    fn out_of_range_leaf_index_rejected() {
690        let entries: Vec<_> = (1..=8u8).map(|i| (xn(i), bh(i))).collect();
691        let tree = MerkleTree::build(entries.clone()).unwrap();
692        let root = tree.root();
693        let (k, _) = &entries[3];
694        let path = tree.path_for(k).unwrap();
695        let lh = leaf_hash(k, &bh(4));
696        // leaf_index >= key_count must be rejected without even hashing.
697        assert!(!verify_path(&lh, &path, 8, 8, &root));
698        assert!(!verify_path(&lh, &path, 99, 8, &root));
699        // Valid baseline.
700        assert!(verify_path(&lh, &path, 3, 8, &root));
701    }
702
703    #[test]
704    fn wrong_path_length_rejected_pre_hashing() {
705        let entries: Vec<_> = (1..=8u8).map(|i| (xn(i), bh(i))).collect();
706        let tree = MerkleTree::build(entries.clone()).unwrap();
707        let root = tree.root();
708        let (k, _) = &entries[3];
709        let path = tree.path_for(k).unwrap();
710        let lh = leaf_hash(k, &bh(4));
711        // For key_count=8 the expected path length is 3 (ceil(log2(8))=3).
712        assert_eq!(path.len(), 3);
713        // Truncating breaks structural check.
714        let short: Vec<_> = path.iter().take(2).copied().collect();
715        assert!(!verify_path(&lh, &short, 3, 8, &root));
716        // Padding too long also breaks structural check.
717        let mut long = path;
718        long.push([0; 32]);
719        assert!(!verify_path(&lh, &long, 3, 8, &root));
720    }
721
722    #[test]
723    fn zero_key_count_rejected() {
724        // Defensive: even with an empty path and correct-shape root, a
725        // commitment claiming zero keys is nonsensical.
726        let lh = [0u8; 32];
727        assert!(!verify_path(&lh, &[], 0, 0, &[0u8; 32]));
728    }
729
730    #[test]
731    fn out_of_protocol_key_count_rejected() {
732        // Wire-supplied key_count exceeding MAX_COMMITMENT_KEY_COUNT is
733        // refused before any hashing. Guards an overflow found in review:
734        // `next_power_of_two()` would otherwise panic in debug and wrap in
735        // release on key_count > 1 << 31.
736        let lh = [0u8; 32];
737        assert!(!verify_path(
738            &lh,
739            &[],
740            0,
741            MAX_COMMITMENT_KEY_COUNT + 1,
742            &[0u8; 32]
743        ));
744        assert!(!verify_path(&lh, &[], 0, u32::MAX, &[0u8; 32]));
745    }
746
747    fn pk_bytes(pk: &MlDsaPublicKey) -> Vec<u8> {
748        pk.to_bytes()
749    }
750
751    #[test]
752    fn sign_and_verify_roundtrip() {
753        let dsa = ml_dsa_65();
754        let (pk, sk) = dsa.generate_keypair().unwrap();
755        let entries: Vec<_> = (0..5u8).map(|i| (xn(i), bh(i))).collect();
756        let tree = MerkleTree::build(entries).unwrap();
757        let root = tree.root();
758        let key_count = tree.key_count();
759        let peer_id = [0xAB; 32];
760        let pk_b = pk_bytes(&pk);
761        let signature = sign_commitment(&sk, &root, key_count, &peer_id, &pk_b).unwrap();
762        let c = StorageCommitment {
763            root,
764            key_count,
765            sender_peer_id: peer_id,
766            sender_public_key: pk_b,
767            signature,
768        };
769        // Verifies via embedded key, no external lookup needed.
770        assert!(verify_commitment_signature(&c));
771    }
772
773    #[test]
774    fn signature_fails_when_root_tampered() {
775        let dsa = ml_dsa_65();
776        let (pk, sk) = dsa.generate_keypair().unwrap();
777        let root = [0u8; 32];
778        let pk_b = pk_bytes(&pk);
779        let signature = sign_commitment(&sk, &root, 1, &[0; 32], &pk_b).unwrap();
780        let c = StorageCommitment {
781            root: [1u8; 32], // tampered
782            key_count: 1,
783            sender_peer_id: [0; 32],
784            sender_public_key: pk_b,
785            signature,
786        };
787        assert!(!verify_commitment_signature(&c));
788    }
789
790    #[test]
791    fn signature_fails_under_swapped_public_key() {
792        let dsa = ml_dsa_65();
793        let (pk1, sk1) = dsa.generate_keypair().unwrap();
794        let (pk2, _sk2) = dsa.generate_keypair().unwrap();
795        let pk1_b = pk_bytes(&pk1);
796        let pk2_b = pk_bytes(&pk2);
797        // Sign under pk1 but embed pk2 — verification (using embedded key)
798        // should fail because pk2 didn't sign this payload AND because the
799        // signed payload binds pk1, not pk2.
800        let signature = sign_commitment(&sk1, &[0u8; 32], 1, &[0; 32], &pk1_b).unwrap();
801        let c = StorageCommitment {
802            root: [0u8; 32],
803            key_count: 1,
804            sender_peer_id: [0; 32],
805            sender_public_key: pk2_b,
806            signature,
807        };
808        assert!(!verify_commitment_signature(&c));
809    }
810
811    #[test]
812    fn signature_fails_with_garbage_bytes() {
813        let dsa = ml_dsa_65();
814        let (pk, _sk) = dsa.generate_keypair().unwrap();
815        let c = StorageCommitment {
816            root: [0u8; 32],
817            key_count: 1,
818            sender_peer_id: [0; 32],
819            sender_public_key: pk_bytes(&pk),
820            signature: vec![0u8; 100], // too short and zero-filled
821        };
822        assert!(!verify_commitment_signature(&c));
823    }
824
825    #[test]
826    fn signature_fails_with_garbage_public_key() {
827        // Embedded pubkey is wrong length / invalid → from_bytes fails →
828        // verify returns false. Defends against malformed gossip.
829        let c = StorageCommitment {
830            root: [0u8; 32],
831            key_count: 1,
832            sender_peer_id: [0; 32],
833            sender_public_key: vec![0u8; 100], // wrong length
834            signature: vec![0u8; 3293],
835        };
836        assert!(!verify_commitment_signature(&c));
837    }
838
839    #[test]
840    fn commitment_hash_differs_on_any_field_change() {
841        let dsa = ml_dsa_65();
842        let (pk, sk) = dsa.generate_keypair().unwrap();
843        let pk_b = pk_bytes(&pk);
844        let sig = sign_commitment(&sk, &[0; 32], 1, &[0; 32], &pk_b).unwrap();
845        let c1 = StorageCommitment {
846            root: [0; 32],
847            key_count: 1,
848            sender_peer_id: [0; 32],
849            sender_public_key: pk_b,
850            signature: sig,
851        };
852        let h1 = commitment_hash(&c1).unwrap();
853
854        let mut c2 = c1.clone();
855        c2.root = [1; 32];
856        assert_ne!(h1, commitment_hash(&c2).unwrap());
857
858        let mut c3 = c1.clone();
859        c3.key_count = 2;
860        assert_ne!(h1, commitment_hash(&c3).unwrap());
861
862        let mut c4 = c1.clone();
863        c4.sender_peer_id = [1; 32];
864        assert_ne!(h1, commitment_hash(&c4).unwrap());
865
866        let mut c5 = c1.clone();
867        c5.signature[0] ^= 1;
868        assert_ne!(h1, commitment_hash(&c5).unwrap());
869
870        let (pk_other, _) = dsa.generate_keypair().unwrap();
871        let mut c6 = c1;
872        c6.sender_public_key = pk_bytes(&pk_other);
873        assert_ne!(h1, commitment_hash(&c6).unwrap());
874    }
875
876    #[test]
877    fn commitment_hash_stable_for_identical_input() {
878        let dsa = ml_dsa_65();
879        let (pk, sk) = dsa.generate_keypair().unwrap();
880        let pk_b = pk_bytes(&pk);
881        let sig = sign_commitment(&sk, &[7; 32], 42, &[3; 32], &pk_b).unwrap();
882        let c = StorageCommitment {
883            root: [7; 32],
884            key_count: 42,
885            sender_peer_id: [3; 32],
886            sender_public_key: pk_b,
887            signature: sig,
888        };
889        assert_eq!(commitment_hash(&c), commitment_hash(&c));
890    }
891
892    #[test]
893    fn commitment_hash_signature_length_change_changes_hash() {
894        // Postcard's varint length prefix means hashing a 1-byte signature
895        // and a 2-byte signature whose first byte is the same produces
896        // different commitment hashes — a hash that omitted the serialized
897        // length prefix would let boundary-shifted fields collide.
898        let c1 = StorageCommitment {
899            root: [0; 32],
900            key_count: 1,
901            sender_peer_id: [0; 32],
902            sender_public_key: vec![0u8; 1952],
903            signature: vec![0xAB],
904        };
905        let c2 = StorageCommitment {
906            root: [0; 32],
907            key_count: 1,
908            sender_peer_id: [0; 32],
909            sender_public_key: vec![0u8; 1952],
910            signature: vec![0xAB, 0x00],
911        };
912        assert_ne!(commitment_hash(&c1).unwrap(), commitment_hash(&c2).unwrap());
913    }
914
915    #[test]
916    fn too_many_keys_rejected() {
917        let mut entries = Vec::with_capacity(MAX_COMMITMENT_KEY_COUNT as usize + 1);
918        for i in 0..=MAX_COMMITMENT_KEY_COUNT {
919            let mut k = [0u8; 32];
920            k[..4].copy_from_slice(&i.to_le_bytes());
921            entries.push((k, [0; 32]));
922        }
923        let result = MerkleTree::build(entries);
924        assert!(matches!(result, Err(CommitmentError::TooManyKeys(_))));
925    }
926}