Skip to content

Commit b252c3d

Browse files
committed
Add nested merkle tree
1 parent 04a5605 commit b252c3d

3 files changed

Lines changed: 156 additions & 54 deletions

File tree

fastcrypto-tbls/src/threshold_schnorr/avid.rs

Lines changed: 42 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
//! to the dealer's broadcast. The set of recipients does not have to be all nodes.
99
1010
use crate::nodes::{Nodes, PartyId};
11+
use crate::threshold_schnorr::merkle::{NestedMerkleProof, NestedMerkleTree};
1112
use crate::threshold_schnorr::reed_solomon::{ErasureCoder, Shard};
1213
use crate::threshold_schnorr::EG;
1314
use crate::types::get_uniform_value;
1415
use fastcrypto::error::FastCryptoError::{InvalidInput, InvalidMessage, NotEnoughWeight};
1516
use fastcrypto::error::FastCryptoResult;
16-
use fastcrypto::hash::Blake2b256;
1717
use fastcrypto::merkle;
18-
use fastcrypto::merkle::MerkleTree;
1918
use itertools::Itertools;
2019
use serde::{Deserialize, Serialize};
2120
use std::collections::{BTreeMap, BTreeSet};
@@ -33,11 +32,13 @@ pub struct Avid {
3332
/// The dealer's per-party dispersal message. One [AuthenticatedShards] per recipient.
3433
pub type Dispersal = BTreeMap<PartyId, AuthenticatedShards>;
3534

36-
/// One disperser's shards for a recipient's payload with a Merkle proof against the `top_root`.
35+
/// One disperser's shards for a recipient's payload with a two-level Merkle proof against the
36+
/// dispersal's `top_root` (row proof to the recipient's row root, top proof binding that row root
37+
/// to `top_root`).
3738
#[derive(Clone, Debug, Serialize, Deserialize)]
3839
pub struct AuthenticatedShards {
3940
pub(crate) shards: Vec<Shard>,
40-
pub(crate) proof: merkle::MerkleProof,
41+
pub(crate) proof: NestedMerkleProof,
4142
}
4243

4344
/// An endorsement of a dispersal's `top_root`.
@@ -96,13 +97,11 @@ impl Avid {
9697
payloads_by_recipient: &BTreeMap<PartyId, Vec<u8>>,
9798
mutate: impl FnOnce(&mut BTreeMap<PartyId, Vec<Vec<Shard>>>),
9899
) -> FastCryptoResult<BTreeMap<PartyId, Dispersal>> {
99-
let code = &self.coder;
100-
101100
// RS-encode each recipient's payload and bucket the shards by disperser.
102101
let mut shards_by_recipient: BTreeMap<PartyId, Vec<Vec<Shard>>> = payloads_by_recipient
103102
.iter()
104103
.map(|(&i, payload)| {
105-
let shards = code.encode(payload)?;
104+
let shards = self.coder.encode(payload)?;
106105
let by_disperser = self.nodes.collect_to_nodes(shards.into_iter())?;
107106
Ok((i, by_disperser))
108107
})
@@ -111,14 +110,9 @@ impl Avid {
111110
#[cfg(test)]
112111
mutate(&mut shards_by_recipient);
113112

114-
// Build a single Merkle tree whose leaves are the (recipient, disperser) cells, laid out
115-
// recipient-major in sorted order — see [Self::leaf_index].
116-
let leaves: Vec<Vec<Shard>> = shards_by_recipient
117-
.values()
118-
.flat_map(|by_disperser| by_disperser.iter().cloned())
119-
.collect();
120-
let tree = MerkleTree::<Blake2b256>::build_from_unserialized(leaves.iter())
121-
.expect("Fails only if serialization fails");
113+
// Two-level Merkle commitment: per-recipient row trees + a top tree over the row roots.
114+
let rows: Vec<Vec<Vec<Shard>>> = shards_by_recipient.values().cloned().collect();
115+
let tree = NestedMerkleTree::<Vec<Shard>>::new(&rows)?;
122116

123117
Ok(self
124118
.nodes
@@ -133,7 +127,7 @@ impl Avid {
133127
AuthenticatedShards {
134128
shards: by_disperser[j as usize].clone(),
135129
proof: tree
136-
.get_proof(self.leaf_index(j, recipient_idx))
130+
.get_proof(recipient_idx, j as usize)
137131
.expect("valid leaf index"),
138132
},
139133
)
@@ -163,11 +157,7 @@ impl Avid {
163157
.map(|(recipient_idx, (_, shards))| {
164158
shards
165159
.proof
166-
.compute_root(
167-
&bcs::to_bytes(&shards.shards).map_err(|_| InvalidInput)?,
168-
self.leaf_index(my_id, recipient_idx),
169-
)
170-
.ok_or(InvalidMessage)
160+
.derive_top_root(&shards.shards, recipient_idx, my_id as usize)
171161
.tap_err(|err| warn!("avid echo: implied root failed at leaf {my_id}: {err:?}"))
172162
})
173163
.collect::<FastCryptoResult<Vec<_>>>()?;
@@ -190,29 +180,29 @@ impl Avid {
190180
pending_recipients: &BTreeSet<PartyId>,
191181
receiver: PartyId,
192182
) -> FastCryptoResult<VerifiedEcho> {
193-
if echo.authenticated_shards.shards.len() != self.nodes.weight_of(sender)? as usize {
183+
let auth = &echo.authenticated_shards;
184+
if auth.shards.len() != self.nodes.weight_of(sender)? as usize {
194185
return Err(InvalidMessage);
195186
}
196187
let receiver_idx = pending_recipients
197188
.iter()
198189
.position(|&id| id == receiver)
199190
.ok_or(InvalidInput)?;
200-
echo.authenticated_shards
201-
.proof
202-
.verify_proof_with_unserialized_leaf(
203-
certified_top_root,
204-
&echo.authenticated_shards.shards,
205-
self.leaf_index(sender, receiver_idx),
206-
)?;
191+
auth.proof.verify(
192+
certified_top_root,
193+
&auth.shards,
194+
receiver_idx,
195+
sender as usize,
196+
)?;
207197
Ok(VerifiedEcho { echo, sender })
208198
}
209199

210200
/// 3b. Reconstruct the caller's payload from a quorum of [VerifiedEcho]s, or raise a
211201
/// [Complaint]. Rejects duplicate dispersers, requires `≥ W − 2f` weight (the RS-decode
212202
/// minimum). With well-formed inputs returns `Ok(Ok(payload))` iff the shards decode to a
213-
/// payload that passes `payload_ok` and re-encoding it reproduces every supplied echo's
214-
/// shards exactly (so the dealer's cells form a valid codeword). Otherwise
215-
/// `Ok(Err(Complaint))` over the shards.
203+
/// payload that passes `payload_ok` and re-encoding it rebuilds a row tree whose root
204+
/// matches the dispersal's `recipient_root` (so the dealer's cells form a valid
205+
/// codeword). Otherwise `Ok(Err(Complaint))` over the shards.
216206
pub fn decode_or_complain(
217207
&self,
218208
echoes: &[VerifiedEcho],
@@ -238,18 +228,28 @@ impl Avid {
238228
Ok(p) if payload_ok(&p) => p,
239229
_ => return Ok(Err(Complaint { shards })),
240230
};
241-
// Re-encode and check every supplied echo's shards lie on the same codeword. Each echo's
242-
// shards were already pinned to the certified `top_root` by [Self::verify_echo], so any
243-
// mismatch here proves the dealer committed cells that are not a valid codeword.
244-
let re_encoded = self
231+
232+
// Re-encode the payload and rebuild the row's Merkle tree.
233+
let re_encoded: Vec<Vec<Shard>> = self
245234
.nodes
246235
.collect_to_nodes(self.coder.encode(&payload)?.into_iter())?;
247-
if shards
248-
.iter()
249-
.any(|(id, auth)| auth.shards != re_encoded[*id as usize])
236+
237+
// Take the expected recipient root from any verified echo's proof, since they all share the same top root and row index.
238+
let (sender, authenticated_shards) =
239+
shards.iter().next().expect("non-empty by check above");
240+
let expected_recipient_root = authenticated_shards
241+
.proof
242+
.row_proof
243+
.compute_root(
244+
&bcs::to_bytes(&authenticated_shards.shards).map_err(|_| InvalidInput)?,
245+
*sender as usize,
246+
)
247+
.ok_or(InvalidMessage)?;
248+
if NestedMerkleTree::<Vec<Shard>>::compute_row_root(&re_encoded)? != expected_recipient_root
250249
{
251250
return Ok(Err(Complaint { shards }));
252251
}
252+
253253
Ok(Ok(payload))
254254
}
255255

@@ -265,14 +265,9 @@ impl Avid {
265265
let Some(accuser_idx) = pending_recipients.iter().position(|&id| id == accuser_id) else {
266266
return Ok(false);
267267
};
268-
let shards_verify = complaint.shards.iter().all(|(&disperser, shards)| {
269-
shards
270-
.proof
271-
.verify_proof_with_unserialized_leaf(
272-
top_root,
273-
&shards.shards,
274-
self.leaf_index(disperser, accuser_idx),
275-
)
268+
let shards_verify = complaint.shards.iter().all(|(&disperser, auth)| {
269+
auth.proof
270+
.verify(top_root, &auth.shards, accuser_idx, disperser as usize)
276271
.is_ok()
277272
});
278273
if !shards_verify
@@ -308,13 +303,6 @@ impl Avid {
308303
fn required_weight(&self) -> u16 {
309304
self.nodes.total_weight() - 2 * self.f
310305
}
311-
312-
/// Position of disperser `sender`'s cell for the recipient at `recipient_idx` (its index in
313-
/// the dispersal's sorted recipient set) in the flat Merkle tree built by
314-
/// [Self::disperse_with_mutation].
315-
fn leaf_index(&self, sender: PartyId, recipient_idx: usize) -> usize {
316-
recipient_idx * self.nodes.num_nodes() + sender as usize
317-
}
318306
}
319307

320308
impl EchoBuilder {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) 2022, Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! A [NestedMerkleTree] commits to a sequence of rows of leaves of type `S`. Each row is
5+
//! committed by its own Merkle tree (its `row_root`). A top tree then commits to those row
6+
//! roots. A [NestedMerkleProof] for a leaf carries both an inclusion proof against its row root
7+
//! and an inclusion proof binding that row root to the top root.
8+
9+
use fastcrypto::error::FastCryptoError::{InvalidInput, InvalidMessage, InvalidProof};
10+
use fastcrypto::error::FastCryptoResult;
11+
use fastcrypto::hash::Blake2b256;
12+
use fastcrypto::merkle::{MerkleProof, MerkleTree, Node};
13+
use serde::{Deserialize, Serialize};
14+
use std::marker::PhantomData;
15+
16+
/// A two-level Merkle commitment to a sequence of rows of `S` leaves.
17+
pub struct NestedMerkleTree<S> {
18+
row_trees: Vec<MerkleTree<Blake2b256>>,
19+
top_tree: MerkleTree<Blake2b256>,
20+
_marker: PhantomData<S>,
21+
}
22+
23+
/// Inclusion proof for one `S` leaf in a [NestedMerkleTree]: row proof up to the (implied) row
24+
/// root, then top proof binding that row root to the dispersal's top root. The intermediate row
25+
/// root is derived from `row_proof` + leaf during verification — not stored.
26+
#[derive(Clone, Debug, Serialize, Deserialize)]
27+
pub struct NestedMerkleProof {
28+
pub row_proof: MerkleProof,
29+
pub top_proof: MerkleProof,
30+
}
31+
32+
#[allow(dead_code)]
33+
impl<S: Serialize> NestedMerkleTree<S> {
34+
/// Build a tree committing to `rows`. `rows[i][j]` is the `j`-th leaf in row `i`.
35+
pub fn new(rows: &[Vec<S>]) -> FastCryptoResult<Self> {
36+
let row_trees: Vec<MerkleTree<Blake2b256>> = rows
37+
.iter()
38+
.map(|row| MerkleTree::<Blake2b256>::build_from_unserialized(row.iter()))
39+
.collect::<FastCryptoResult<_>>()?;
40+
let row_roots: Vec<Node> = row_trees.iter().map(|t| t.root()).collect();
41+
let top_tree = MerkleTree::<Blake2b256>::build_from_unserialized(row_roots.iter())?;
42+
Ok(Self {
43+
row_trees,
44+
top_tree,
45+
_marker: PhantomData,
46+
})
47+
}
48+
49+
/// The top tree's root — the overall commitment.
50+
pub fn top_root(&self) -> Node {
51+
self.top_tree.root()
52+
}
53+
54+
/// Root of the row tree for row `row_idx`, or `None` if out of bounds.
55+
pub fn row_root(&self, row_idx: usize) -> Option<Node> {
56+
self.row_trees.get(row_idx).map(|t| t.root())
57+
}
58+
59+
/// Inclusion proof for the leaf at position `leaf_idx` in row `row_idx`.
60+
pub fn get_proof(
61+
&self,
62+
row_idx: usize,
63+
leaf_idx: usize,
64+
) -> FastCryptoResult<NestedMerkleProof> {
65+
let row_tree = self.row_trees.get(row_idx).ok_or(InvalidInput)?;
66+
Ok(NestedMerkleProof {
67+
row_proof: row_tree.get_proof(leaf_idx)?,
68+
top_proof: self.top_tree.get_proof(row_idx)?,
69+
})
70+
}
71+
72+
/// Compute the root of a row's Merkle subtree, byte-identical to what [Self::new] would build
73+
/// for the same row.
74+
pub fn compute_row_root(row: &[S]) -> FastCryptoResult<Node> {
75+
Ok(MerkleTree::<Blake2b256>::build_from_unserialized(row.iter())?.root())
76+
}
77+
}
78+
79+
impl NestedMerkleProof {
80+
/// Verify this proof against `top_root` for `leaf` at position (`row_idx`, `leaf_idx`).
81+
pub fn verify<S: Serialize>(
82+
&self,
83+
top_root: &Node,
84+
leaf: &S,
85+
row_idx: usize,
86+
leaf_idx: usize,
87+
) -> FastCryptoResult<()> {
88+
(self.derive_top_root(leaf, row_idx, leaf_idx)? == *top_root)
89+
.then_some(())
90+
.ok_or(InvalidProof)
91+
}
92+
93+
/// Derive the implied top root from this proof and `leaf` at (`row_idx`, `leaf_idx`): walk
94+
/// the row proof from the leaf to an implied row root, then walk the top proof from that row
95+
/// root to an implied top root. Returns [InvalidMessage] if either path is malformed.
96+
pub fn derive_top_root<S: Serialize>(
97+
&self,
98+
leaf: &S,
99+
row_idx: usize,
100+
leaf_idx: usize,
101+
) -> FastCryptoResult<Node> {
102+
let row_root = self
103+
.row_proof
104+
.compute_root(&bcs::to_bytes(leaf).map_err(|_| InvalidInput)?, leaf_idx)
105+
.ok_or(InvalidMessage)?;
106+
self.top_proof
107+
.compute_root(
108+
&bcs::to_bytes(&row_root).map_err(|_| InvalidInput)?,
109+
row_idx,
110+
)
111+
.ok_or(InvalidMessage)
112+
}
113+
}

fastcrypto-tbls/src/threshold_schnorr/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub mod batch_avss_avid;
4646
mod bcs;
4747
pub mod complaint;
4848
pub mod key_derivation;
49+
mod merkle;
4950
mod pascal_matrix;
5051
pub mod presigning;
5152
pub mod recovery_proof;

0 commit comments

Comments
 (0)