Skip to content

Commit 8a1f604

Browse files
[coding] Use MMR multi-proofs for ZODA (#2215)
Co-authored-by: Patrick O'Grady <me@patrickogrady.xyz>
1 parent d2db70b commit 8a1f604

3 files changed

Lines changed: 53 additions & 49 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.

coding/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ commonware-codec = { workspace = true, features = ["std"] }
1616
commonware-cryptography = { workspace = true, features = ["std"] }
1717
commonware-storage = { workspace = true, features = ["std"] }
1818
commonware-utils = { workspace = true, features = ["std"] }
19+
futures.workspace = true
1920
num-rational.workspace = true
2021
rand.workspace = true
2122
rand_core.workspace = true

coding/src/zoda.rs

Lines changed: 51 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,11 @@ use commonware_cryptography::{
124124
transcript::{Summary, Transcript},
125125
Hasher,
126126
};
127-
use commonware_storage::bmt::{Builder, Proof};
128-
use commonware_utils::rational::BigRationalExt;
127+
use commonware_storage::mmr::{
128+
mem::Mmr, verification::multi_proof, Error as MmrError, Location, Proof, StandardHasher,
129+
};
130+
use commonware_utils::BigRationalExt as _;
131+
use futures::executor::block_on;
129132
use num_rational::BigRational;
130133
use rand::seq::SliceRandom as _;
131134
use std::{marker::PhantomData, sync::Arc};
@@ -294,7 +297,7 @@ impl Topology {
294297
pub struct Shard<H: Hasher> {
295298
data_bytes: usize,
296299
root: H::Digest,
297-
inclusion_proofs: Vec<Proof<H>>,
300+
inclusion_proof: Proof<H::Digest>,
298301
rows: Matrix,
299302
checksum: Arc<Matrix>,
300303
}
@@ -303,7 +306,7 @@ impl<H: Hasher> PartialEq for Shard<H> {
303306
fn eq(&self, other: &Self) -> bool {
304307
self.data_bytes == other.data_bytes
305308
&& self.root == other.root
306-
&& self.inclusion_proofs == other.inclusion_proofs
309+
&& self.inclusion_proof == other.inclusion_proof
307310
&& self.rows == other.rows
308311
&& self.checksum == other.checksum
309312
}
@@ -315,7 +318,7 @@ impl<H: Hasher> EncodeSize for Shard<H> {
315318
fn encode_size(&self) -> usize {
316319
self.data_bytes.encode_size()
317320
+ self.root.encode_size()
318-
+ self.inclusion_proofs.encode_size()
321+
+ self.inclusion_proof.encode_size()
319322
+ self.rows.encode_size()
320323
+ self.checksum.encode_size()
321324
}
@@ -325,7 +328,7 @@ impl<H: Hasher> Write for Shard<H> {
325328
fn write(&self, buf: &mut impl BufMut) {
326329
self.data_bytes.write(buf);
327330
self.root.write(buf);
328-
self.inclusion_proofs.write(buf);
331+
self.inclusion_proof.write(buf);
329332
self.rows.write(buf);
330333
self.checksum.write(buf);
331334
}
@@ -343,7 +346,7 @@ impl<H: Hasher> Read for Shard<H> {
343346
Ok(Self {
344347
data_bytes,
345348
root: ReadExt::read(buf)?,
346-
inclusion_proofs: Read::read_cfg(buf, &(RangeCfg::from(..=max_els), ()))?,
349+
inclusion_proof: Read::read_cfg(buf, &max_els)?,
347350
rows: Read::read_cfg(buf, &max_els)?,
348351
checksum: Arc::new(Read::read_cfg(buf, &max_els)?),
349352
})
@@ -352,27 +355,27 @@ impl<H: Hasher> Read for Shard<H> {
352355

353356
#[derive(Clone, Debug)]
354357
pub struct ReShard<H: Hasher> {
355-
inclusion_proofs: Vec<Proof<H>>,
358+
inclusion_proof: Proof<H::Digest>,
356359
shard: Matrix,
357360
}
358361

359362
impl<H: Hasher> PartialEq for ReShard<H> {
360363
fn eq(&self, other: &Self) -> bool {
361-
self.inclusion_proofs == other.inclusion_proofs && self.shard == other.shard
364+
self.inclusion_proof == other.inclusion_proof && self.shard == other.shard
362365
}
363366
}
364367

365368
impl<H: Hasher> Eq for ReShard<H> {}
366369

367370
impl<H: Hasher> EncodeSize for ReShard<H> {
368371
fn encode_size(&self) -> usize {
369-
self.inclusion_proofs.encode_size() + self.shard.encode_size()
372+
self.inclusion_proof.encode_size() + self.shard.encode_size()
370373
}
371374
}
372375

373376
impl<H: Hasher> Write for ReShard<H> {
374377
fn write(&self, buf: &mut impl BufMut) {
375-
self.inclusion_proofs.write(buf);
378+
self.inclusion_proof.write(buf);
376379
self.shard.write(buf);
377380
}
378381
}
@@ -388,7 +391,7 @@ impl<H: Hasher> Read for ReShard<H> {
388391
let max_data_els = F::bits_to_elements(max_data_bits).max(1);
389392
Ok(Self {
390393
// Worst case: every row is one data element, and the sample size is all rows.
391-
inclusion_proofs: Read::read_cfg(buf, &(RangeCfg::from(..=max_data_els), ()))?,
394+
inclusion_proof: Read::read_cfg(buf, &max_data_els)?,
392395
shard: Read::read_cfg(buf, &max_data_els)?,
393396
})
394397
}
@@ -403,8 +406,8 @@ pub struct CheckedShard {
403406
/// Take indices up to `total`, and shuffle them.
404407
///
405408
/// The shuffle depends, deterministically, on the transcript.
406-
fn shuffle_indices(transcript: &Transcript, total: usize) -> Vec<usize> {
407-
let mut out = (0..total).collect::<Vec<_>>();
409+
fn shuffle_indices(transcript: &Transcript, total: usize) -> Vec<Location> {
410+
let mut out = (0..total as u64).map(Location::from).collect::<Vec<_>>();
408411
out.shuffle(&mut transcript.noise(b"shuffle"));
409412
out
410413
}
@@ -427,7 +430,7 @@ pub struct CheckingData<H: Hasher> {
427430
root: H::Digest,
428431
checking_matrix: Matrix,
429432
encoded_checksum: Matrix,
430-
shuffled_indices: Vec<usize>,
433+
shuffled_indices: Vec<Location>,
431434
}
432435

433436
impl<H: Hasher> CheckingData<H> {
@@ -479,32 +482,30 @@ impl<H: Hasher> CheckingData<H> {
479482
self.topology.check_index(index)?;
480483
if reshard.shard.rows() != self.topology.samples
481484
|| reshard.shard.cols() != self.topology.data_cols
482-
|| reshard.inclusion_proofs.len() != reshard.shard.rows()
483485
{
484486
return Err(Error::InvalidReShard);
485487
}
486488
let index = index as usize;
487489
let these_shuffled_indices = &self.shuffled_indices
488490
[index * self.topology.samples..(index + 1) * self.topology.samples];
489-
// Check every inclusion proof.
490-
for ((&i, row), proof) in these_shuffled_indices
491-
.iter()
492-
.zip(reshard.shard.iter())
493-
.zip(&reshard.inclusion_proofs)
494-
{
495-
proof
496-
.verify(
497-
&mut H::new(),
498-
&F::slice_digest::<H>(row),
499-
i as u32,
500-
&self.root,
501-
)
502-
.map_err(|_| Error::InvalidReShard)?;
491+
let proof_elements = {
492+
these_shuffled_indices
493+
.iter()
494+
.zip(reshard.shard.iter())
495+
.map(|(&i, row)| (F::slice_digest::<H>(row), i))
496+
.collect::<Vec<_>>()
497+
};
498+
if !reshard.inclusion_proof.verify_multi_inclusion(
499+
&mut StandardHasher::<H>::new(),
500+
&proof_elements,
501+
&self.root,
502+
) {
503+
return Err(Error::InvalidReShard);
503504
}
504505
let shard_checksum = reshard.shard.mul(&self.checking_matrix);
505506
// Check that the shard checksum rows match the encoded checksums
506507
for (row, &i) in shard_checksum.iter().zip(these_shuffled_indices) {
507-
if row != &self.encoded_checksum[i] {
508+
if row != &self.encoded_checksum[u64::from(i) as usize] {
508509
return Err(Error::InvalidReShard);
509510
}
510511
}
@@ -527,6 +528,8 @@ pub enum Error {
527528
InsufficientShards(usize, usize),
528529
#[error("insufficient unique rows {0} < {1}")]
529530
InsufficientUniqueRows(usize, usize),
531+
#[error("failed to create inclusion proof: {0}")]
532+
FailedToCreateInclusionProof(MmrError),
530533
}
531534

532535
const NAMESPACE: &[u8] = b"commonware-zoda";
@@ -567,19 +570,23 @@ impl<H: Hasher> Scheme for Zoda<H> {
567570
topology.data_cols,
568571
F::stream_from_u64s(iter_u64_le(data)),
569572
);
573+
570574
// Step 2: Encode the data.
571575
let encoded_data = data
572576
.as_polynomials(topology.encoded_rows)
573577
.expect("data has too many rows")
574578
.evaluate()
575579
.data();
580+
576581
// Step 3: Commit to the rows of the data.
577-
let mut builder = Builder::<H>::new(encoded_data.rows());
582+
let mut hasher = StandardHasher::<H>::new();
583+
let mut mmr = Mmr::new();
578584
for row in encoded_data.iter() {
579-
builder.add(&F::slice_digest::<H>(row));
585+
mmr.add_batched(&mut hasher, &F::slice_digest::<H>(row));
580586
}
581-
let tree = builder.build();
582-
let root = tree.root();
587+
let mmr = mmr.merkleize(&mut hasher);
588+
let root = mmr.root(&mut hasher);
589+
583590
// Step 4: Commit to the root, and the size of the data.
584591
let mut transcript = Transcript::new(NAMESPACE);
585592
transcript.commit((topology.data_bytes as u64).encode());
@@ -604,24 +611,19 @@ impl<H: Hasher> Scheme for Zoda<H> {
604611
topology.data_cols,
605612
indices
606613
.iter()
607-
.flat_map(|&i| encoded_data[i].iter().copied()),
614+
.flat_map(|&i| encoded_data[u64::from(i) as usize].iter().copied()),
608615
);
609-
let inclusion_proofs = indices
610-
.iter()
611-
.map(|&i| {
612-
tree.proof(i as u32)
613-
.expect("Impossible: ZODA should always have at least 1 row")
614-
})
615-
.collect::<Vec<_>>();
616-
Shard {
616+
let inclusion_proof = block_on(multi_proof(&mmr, indices))
617+
.map_err(Error::FailedToCreateInclusionProof)?;
618+
Ok(Shard {
617619
data_bytes,
618620
root,
619-
inclusion_proofs,
621+
inclusion_proof,
620622
rows,
621623
checksum: checksum.clone(),
622-
}
624+
})
623625
})
624-
.collect::<Vec<_>>();
626+
.collect::<Result<Vec<_>, Error>>()?;
625627
Ok((commitment, shards))
626628
}
627629

@@ -632,7 +634,7 @@ impl<H: Hasher> Scheme for Zoda<H> {
632634
shard: Self::Shard,
633635
) -> Result<(Self::CheckingData, Self::CheckedShard, Self::ReShard), Self::Error> {
634636
let reshard = ReShard {
635-
inclusion_proofs: shard.inclusion_proofs,
637+
inclusion_proof: shard.inclusion_proof,
636638
shard: shard.rows,
637639
};
638640
let checking_data = CheckingData::reckon(
@@ -679,7 +681,7 @@ impl<H: Hasher> Scheme for Zoda<H> {
679681
let indices =
680682
&checking_data.shuffled_indices[shard.index * samples..(shard.index + 1) * samples];
681683
for (&i, row) in indices.iter().zip(shard.shard.iter()) {
682-
evaluation.fill_row(i, row);
684+
evaluation.fill_row(u64::from(i) as usize, row);
683685
}
684686
}
685687
// This should never happen, because we check each shard, and the shards

0 commit comments

Comments
 (0)