1use crate::{Config, PhasedScheme, ValidatingScheme};
118use bytes::BufMut;
119use commonware_codec::{Encode, EncodeSize, FixedSize, RangeCfg, Read, ReadExt, Write};
120use commonware_cryptography::{
121 transcript::{Summary, Transcript},
122 Digest, Hasher,
123};
124use commonware_math::{
125 fields::goldilocks::F,
126 ntt::{EvaluationVector, Matrix},
127};
128use commonware_parallel::Strategy;
129use commonware_storage::bmt::{Builder as BmtBuilder, Error as BmtError, Proof};
130use std::{marker::PhantomData, sync::Arc};
131use thiserror::Error;
132
133fn iter_u64_le(data: impl bytes::Buf) -> impl Iterator<Item = u64> {
135 struct Iter<B> {
136 remaining_u64s: usize,
137 tail: usize,
138 inner: B,
139 }
140
141 impl<B: bytes::Buf> Iter<B> {
142 fn new(inner: B) -> Self {
143 let remaining_u64s = inner.remaining() / 8;
144 let tail = inner.remaining() % 8;
145 Self {
146 remaining_u64s,
147 tail,
148 inner,
149 }
150 }
151 }
152
153 impl<B: bytes::Buf> Iterator for Iter<B> {
154 type Item = u64;
155
156 fn next(&mut self) -> Option<Self::Item> {
157 if self.remaining_u64s > 0 {
158 self.remaining_u64s -= 1;
159 return Some(self.inner.get_u64_le());
160 }
161 if self.tail > 0 {
162 let mut chunk = [0u8; 8];
163 self.inner.copy_to_slice(&mut chunk[..self.tail]);
164 self.tail = 0;
165 return Some(u64::from_le_bytes(chunk));
166 }
167 None
168 }
169 }
170 Iter::new(data)
171}
172
173fn collect_u64_le(max_length: usize, data: impl Iterator<Item = u64>) -> Vec<u8> {
174 let mut out = Vec::with_capacity(max_length);
175 for d in data {
176 out.extend_from_slice(&d.to_le_bytes());
177 }
178 out.truncate(max_length);
179 out
180}
181
182fn row_digest<H: Hasher>(row: &[F]) -> H::Digest {
183 let mut h = H::new();
184 for x in row {
185 h.update(&x.to_le_bytes());
186 }
187 h.finalize()
188}
189
190mod topology;
191use topology::Topology;
192
193#[derive(Clone, Debug)]
195pub struct StrongShard<D: Digest> {
196 data_bytes: usize,
197 root: D,
198 inclusion_proof: Proof<D>,
199 rows: Matrix<F>,
200 checksum: Arc<Matrix<F>>,
201}
202
203impl<D: Digest> PartialEq for StrongShard<D> {
204 fn eq(&self, other: &Self) -> bool {
205 self.data_bytes == other.data_bytes
206 && self.root == other.root
207 && self.inclusion_proof == other.inclusion_proof
208 && self.rows == other.rows
209 && self.checksum == other.checksum
210 }
211}
212
213impl<D: Digest> Eq for StrongShard<D> {}
214
215impl<D: Digest> EncodeSize for StrongShard<D> {
216 fn encode_size(&self) -> usize {
217 self.data_bytes.encode_size()
218 + self.root.encode_size()
219 + self.inclusion_proof.encode_size()
220 + self.rows.encode_size()
221 + self.checksum.encode_size()
222 }
223}
224
225impl<D: Digest> Write for StrongShard<D> {
226 fn write(&self, buf: &mut impl BufMut) {
227 self.data_bytes.write(buf);
228 self.root.write(buf);
229 self.inclusion_proof.write(buf);
230 self.rows.write(buf);
231 self.checksum.write(buf);
232 }
233}
234
235impl<D: Digest> Read for StrongShard<D> {
236 type Cfg = crate::CodecConfig;
237
238 fn read_cfg(
239 buf: &mut impl bytes::Buf,
240 cfg: &Self::Cfg,
241 ) -> Result<Self, commonware_codec::Error> {
242 let data_bytes = usize::read_cfg(buf, &RangeCfg::from(..=cfg.maximum_shard_size))?;
243 let max_els = cfg.maximum_shard_size / F::SIZE;
244 Ok(Self {
245 data_bytes,
246 root: ReadExt::read(buf)?,
247 inclusion_proof: Read::read_cfg(buf, &max_els)?,
248 rows: Read::read_cfg(buf, &(max_els, ()))?,
249 checksum: Arc::new(Read::read_cfg(buf, &(max_els, ()))?),
250 })
251 }
252}
253
254#[cfg(feature = "arbitrary")]
255impl<D: Digest> arbitrary::Arbitrary<'_> for StrongShard<D>
256where
257 D: for<'a> arbitrary::Arbitrary<'a>,
258{
259 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
260 Ok(Self {
261 data_bytes: u.arbitrary::<u32>()? as usize,
262 root: u.arbitrary()?,
263 inclusion_proof: u.arbitrary()?,
264 rows: u.arbitrary()?,
265 checksum: Arc::new(u.arbitrary()?),
266 })
267 }
268}
269
270#[derive(Clone, Debug)]
271pub struct WeakShard<D: Digest> {
272 inclusion_proof: Proof<D>,
273 shard: Matrix<F>,
274}
275
276impl<D: Digest> PartialEq for WeakShard<D> {
277 fn eq(&self, other: &Self) -> bool {
278 self.inclusion_proof == other.inclusion_proof && self.shard == other.shard
279 }
280}
281
282impl<D: Digest> Eq for WeakShard<D> {}
283
284impl<D: Digest> EncodeSize for WeakShard<D> {
285 fn encode_size(&self) -> usize {
286 self.inclusion_proof.encode_size() + self.shard.encode_size()
287 }
288}
289
290impl<D: Digest> Write for WeakShard<D> {
291 fn write(&self, buf: &mut impl BufMut) {
292 self.inclusion_proof.write(buf);
293 self.shard.write(buf);
294 }
295}
296
297impl<D: Digest> Read for WeakShard<D> {
298 type Cfg = crate::CodecConfig;
299
300 fn read_cfg(
301 buf: &mut impl bytes::Buf,
302 cfg: &Self::Cfg,
303 ) -> Result<Self, commonware_codec::Error> {
304 let max_data_bits = cfg.maximum_shard_size.saturating_mul(8);
305 let max_data_els = F::bits_to_elements(max_data_bits).max(1);
306 Ok(Self {
307 inclusion_proof: Read::read_cfg(buf, &max_data_els)?,
309 shard: Read::read_cfg(buf, &(max_data_els, ()))?,
310 })
311 }
312}
313
314#[cfg(feature = "arbitrary")]
315impl<D: Digest> arbitrary::Arbitrary<'_> for WeakShard<D>
316where
317 D: for<'a> arbitrary::Arbitrary<'a>,
318{
319 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
320 Ok(Self {
321 inclusion_proof: u.arbitrary()?,
322 shard: u.arbitrary()?,
323 })
324 }
325}
326
327#[derive(Clone)]
329pub struct CheckedShard {
330 index: usize,
331 shard: Matrix<F>,
332 commitment: Summary,
333}
334
335fn shuffle_indices(transcript: &Transcript, total: usize) -> Vec<u32> {
343 let total: u32 = total
344 .try_into()
345 .expect("encoded_rows exceeds u32::MAX; data too large for ZODA");
346 let mut out = (0..total).collect::<Vec<_>>();
347 transcript.shuffle(b"shuffle", &mut out);
348 out
349}
350
351fn checking_matrix(transcript: &Transcript, topology: &Topology) -> Matrix<F> {
355 Matrix::rand(
356 transcript.noise(b"checking matrix"),
357 topology.data_cols,
358 topology.column_samples,
359 )
360}
361
362#[derive(Clone, PartialEq)]
364pub struct CheckingData<D: Digest> {
365 commitment: Summary,
366 topology: Topology,
367 root: D,
368 checking_matrix: Matrix<F>,
369 encoded_checksum: Matrix<F>,
370 shuffled_indices: Vec<u32>,
371}
372
373impl<D: Digest> Eq for CheckingData<D> {}
374
375impl<D: Digest> CheckingData<D> {
376 fn reckon(
385 namespace: &[u8],
386 config: &Config,
387 commitment: &Summary,
388 data_bytes: usize,
389 root: D,
390 checksum: &Matrix<F>,
391 ) -> Result<Self, Error> {
392 let topology = Topology::reckon(config, data_bytes);
393 let mut transcript = Transcript::new(NAMESPACE);
394 transcript.commit(namespace);
395 transcript.commit((topology.data_bytes as u64).encode());
396 transcript.commit(root.encode());
397 let expected_commitment = transcript.summarize();
398 if *commitment != expected_commitment {
399 return Err(Error::InvalidShard);
400 }
401 let mut transcript = Transcript::resume(expected_commitment);
402 let checking_matrix = checking_matrix(&transcript, &topology);
403 if checksum.rows() != topology.data_rows || checksum.cols() != topology.column_samples {
404 return Err(Error::InvalidShard);
405 }
406 transcript.commit(checksum.encode());
414 let encoded_checksum = checksum
415 .as_polynomials(topology.encoded_rows)
416 .expect("checksum has too many rows")
417 .evaluate()
418 .data();
419 let shuffled_indices = shuffle_indices(&transcript, topology.encoded_rows);
420
421 Ok(Self {
422 commitment: expected_commitment,
423 topology,
424 root,
425 checking_matrix,
426 encoded_checksum,
427 shuffled_indices,
428 })
429 }
430
431 fn check<H: Hasher<Digest = D>>(
432 &self,
433 commitment: &Summary,
434 index: u16,
435 weak_shard: &WeakShard<D>,
436 ) -> Result<CheckedShard, Error> {
437 if self.commitment != *commitment {
438 return Err(Error::InvalidShard);
439 }
440 self.topology.check_index(index)?;
441 if weak_shard.shard.rows() != self.topology.samples
442 || weak_shard.shard.cols() != self.topology.data_cols
443 {
444 return Err(Error::InvalidWeakShard);
445 }
446 let index = index as usize;
447 let these_shuffled_indices = &self.shuffled_indices
448 [index * self.topology.samples..(index + 1) * self.topology.samples];
449
450 let proof_elements: Vec<(H::Digest, u32)> = these_shuffled_indices
453 .iter()
454 .zip(weak_shard.shard.iter())
455 .map(|(&i, row)| (row_digest::<H>(row), i))
456 .collect();
457
458 let mut hasher = H::new();
460 if weak_shard
461 .inclusion_proof
462 .verify_multi_inclusion(&mut hasher, &proof_elements, &self.root)
463 .is_err()
464 {
465 return Err(Error::InvalidWeakShard);
466 }
467
468 let shard_checksum = weak_shard.shard.mul(&self.checking_matrix);
469 for (row, &i) in shard_checksum.iter().zip(these_shuffled_indices) {
471 if row != &self.encoded_checksum[i as usize] {
472 return Err(Error::InvalidWeakShard);
473 }
474 }
475 Ok(CheckedShard {
476 index,
477 shard: weak_shard.shard.clone(),
478 commitment: *commitment,
479 })
480 }
481}
482
483#[derive(Debug, Error)]
484pub enum Error {
485 #[error("invalid shard")]
486 InvalidShard,
487 #[error("invalid weak shard")]
488 InvalidWeakShard,
489 #[error("invalid index {0}")]
490 InvalidIndex(u16),
491 #[error("insufficient shards {0} < {1}")]
492 InsufficientShards(usize, usize),
493 #[error("insufficient unique rows {0} < {1}")]
494 InsufficientUniqueRows(usize, usize),
495 #[error("failed to create inclusion proof: {0}")]
496 FailedToCreateInclusionProof(BmtError),
497}
498
499const NAMESPACE: &[u8] = b"_COMMONWARE_CODING_ZODA";
500
501#[derive(Clone, Copy)]
502pub struct Zoda<H> {
503 _marker: PhantomData<H>,
504}
505
506impl<H> std::fmt::Debug for Zoda<H> {
507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508 write!(f, "Zoda")
509 }
510}
511
512impl<H: Hasher> PhasedScheme for Zoda<H> {
513 type Commitment = Summary;
514 type StrongShard = StrongShard<H::Digest>;
515 type WeakShard = WeakShard<H::Digest>;
516 type CheckingData = CheckingData<H::Digest>;
517 type CheckedShard = CheckedShard;
518 type Error = Error;
519
520 fn encode(
521 namespace: &[u8],
522 config: &Config,
523 data: impl bytes::Buf,
524 strategy: &impl Strategy,
525 ) -> Result<(Self::Commitment, Vec<Self::StrongShard>), Self::Error> {
526 let data_bytes = data.remaining();
528 let topology = Topology::reckon(config, data_bytes);
529 let data = Matrix::init(
530 topology.data_rows,
531 topology.data_cols,
532 F::stream_from_u64s(iter_u64_le(data)),
533 );
534
535 let encoded_data = data
537 .as_polynomials(topology.encoded_rows)
538 .expect("data has too many rows")
539 .evaluate()
540 .data();
541
542 let row_hashes: Vec<H::Digest> = strategy.map_collect_vec(0..encoded_data.rows(), |i| {
544 row_digest::<H>(&encoded_data[i])
545 });
546 let mut bmt_builder = BmtBuilder::<H>::new(row_hashes.len());
547 for hash in &row_hashes {
548 bmt_builder.add(hash);
549 }
550 let bmt = bmt_builder.build();
551 let root = bmt.root();
552
553 let mut transcript = Transcript::new(NAMESPACE);
555 transcript.commit(namespace);
556 transcript.commit((topology.data_bytes as u64).encode());
557 transcript.commit(root.encode());
558 let commitment = transcript.summarize();
559
560 let mut transcript = Transcript::resume(commitment);
562 let checking_matrix = checking_matrix(&transcript, &topology);
563 let checksum = Arc::new(data.mul(&checking_matrix));
564 transcript.commit(checksum.encode());
569 let shuffled_indices = shuffle_indices(&transcript, encoded_data.rows());
570
571 let shards = strategy.try_map_collect_vec(0..topology.total_shards, |shard_idx| {
573 let indices =
574 &shuffled_indices[shard_idx * topology.samples..(shard_idx + 1) * topology.samples];
575 let rows = Matrix::init(
576 indices.len(),
577 topology.data_cols,
578 indices
579 .iter()
580 .flat_map(|&i| encoded_data[i as usize].iter().copied()),
581 );
582 let inclusion_proof = bmt
583 .multi_proof(indices)
584 .map_err(Error::FailedToCreateInclusionProof)?;
585 Ok(StrongShard {
586 data_bytes,
587 root,
588 inclusion_proof,
589 rows,
590 checksum: checksum.clone(),
591 })
592 })?;
593 Ok((commitment, shards))
594 }
595
596 fn weaken(
597 namespace: &[u8],
598 config: &Config,
599 commitment: &Self::Commitment,
600 index: u16,
601 shard: Self::StrongShard,
602 ) -> Result<(Self::CheckingData, Self::CheckedShard, Self::WeakShard), Self::Error> {
603 let weak_shard = WeakShard {
604 inclusion_proof: shard.inclusion_proof,
605 shard: shard.rows,
606 };
607 let checking_data = CheckingData::reckon(
608 namespace,
609 config,
610 commitment,
611 shard.data_bytes,
612 shard.root,
613 shard.checksum.as_ref(),
614 )?;
615 let checked_shard = checking_data.check::<H>(commitment, index, &weak_shard)?;
616 Ok((checking_data, checked_shard, weak_shard))
617 }
618
619 fn check(
620 _config: &Config,
621 commitment: &Self::Commitment,
622 checking_data: &Self::CheckingData,
623 index: u16,
624 weak_shard: Self::WeakShard,
625 ) -> Result<Self::CheckedShard, Self::Error> {
626 checking_data.check::<H>(commitment, index, &weak_shard)
627 }
628
629 fn decode<'a>(
630 _config: &Config,
631 commitment: &Self::Commitment,
632 checking_data: Self::CheckingData,
633 shards: impl Iterator<Item = &'a Self::CheckedShard>,
634 _strategy: &impl Strategy,
635 ) -> Result<Vec<u8>, Self::Error> {
636 if checking_data.commitment != *commitment {
637 return Err(Error::InvalidShard);
638 }
639
640 let Topology {
641 encoded_rows,
642 data_cols,
643 samples,
644 data_rows,
645 data_bytes,
646 min_shards,
647 ..
648 } = checking_data.topology;
649 let mut evaluation = EvaluationVector::<F>::empty(encoded_rows.ilog2() as usize, data_cols);
650 let mut shard_count = 0usize;
651 for shard in shards {
652 shard_count += 1;
653 if shard.commitment != *commitment {
654 return Err(Error::InvalidShard);
655 }
656 let indices =
657 &checking_data.shuffled_indices[shard.index * samples..(shard.index + 1) * samples];
658 for (&i, row) in indices.iter().zip(shard.shard.iter()) {
659 evaluation.fill_row(u64::from(i) as usize, row);
660 }
661 }
662 if shard_count < min_shards {
663 return Err(Error::InsufficientShards(shard_count, min_shards));
664 }
665 let filled_rows = evaluation.filled_rows();
668 if filled_rows < data_rows {
669 return Err(Error::InsufficientUniqueRows(filled_rows, data_rows));
670 }
671 Ok(collect_u64_le(
672 data_bytes,
673 F::stream_to_u64s(
674 evaluation
675 .recover()
676 .coefficients_up_to(data_rows)
677 .flatten()
678 .copied(),
679 ),
680 ))
681 }
682}
683
684impl<H: Hasher> ValidatingScheme for Zoda<H> {}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use crate::{Config, PhasedScheme};
690 use commonware_cryptography::Sha256;
691 use commonware_math::{
692 algebra::{FieldNTT as _, Ring as _},
693 ntt::PolynomialVector,
694 };
695 use commonware_parallel::Sequential;
696 use commonware_utils::NZU16;
697
698 const STRATEGY: Sequential = Sequential;
699
700 #[test]
701 fn decode_rejects_duplicate_indices() {
702 let config = Config {
703 minimum_shards: NZU16!(2),
704 extra_shards: NZU16!(1),
705 };
706 let data = b"duplicate shard coverage";
707 let (commitment, shards) =
708 Zoda::<Sha256>::encode(b"", &config, &data[..], &STRATEGY).unwrap();
709 let shard0 = shards[0].clone();
710 let (checking_data, checked_shard0, _weak_shard0) =
711 Zoda::<Sha256>::weaken(b"", &config, &commitment, 0, shard0).unwrap();
712 let duplicate = CheckedShard {
713 index: checked_shard0.index,
714 shard: checked_shard0.shard.clone(),
715 commitment: checked_shard0.commitment,
716 };
717 let shards = [checked_shard0, duplicate];
718 let result = Zoda::<Sha256>::decode(
719 &config,
720 &commitment,
721 checking_data,
722 shards.iter(),
723 &STRATEGY,
724 );
725 match result {
726 Err(Error::InsufficientUniqueRows(actual, expected)) => {
727 assert!(actual < expected);
728 }
729 other => panic!("expected insufficient unique rows error, got {other:?}"),
730 }
731 }
732
733 #[test]
734 fn checksum_malleability() {
735 fn vanishing(lg_domain: u8, vanish_indices: &[u32]) -> PolynomialVector<F> {
739 let w = F::root_of_unity(lg_domain).expect("domain too large for Goldilocks");
740 let mut domain = Vec::with_capacity(1usize << lg_domain);
741 let mut x = F::one();
742 for _ in 0..(1usize << lg_domain) {
743 domain.push(x);
744 x *= &w;
745 }
746 let roots: Vec<F> = vanish_indices.iter().map(|&i| domain[i as usize]).collect();
747 let mut out = EvaluationVector::empty(lg_domain as usize, 1);
748 domain.into_iter().enumerate().for_each(|(i, x)| {
749 let mut acc = F::one();
750 for root in &roots {
751 acc *= &(x - root);
752 }
753 out.fill_row(i, &[acc]);
754 });
755 out.recover()
756 }
757
758 let config = Config {
759 minimum_shards: NZU16!(2),
760 extra_shards: NZU16!(1),
761 };
762 let data = vec![0x5Au8; 256 * 1024];
763 let (commitment, mut shards) =
764 Zoda::<Sha256>::encode(b"", &config, &data[..], &STRATEGY).unwrap();
765
766 let leader_i = 0usize;
767 let a_i = 1usize;
768 let b_i = 2usize;
769
770 {
772 let (checking_data, _, _) = Zoda::<Sha256>::weaken(
773 b"",
774 &config,
775 &commitment,
776 leader_i as u16,
777 shards[leader_i].clone(),
778 )
779 .unwrap();
780
781 let samples = checking_data.topology.samples;
782 let a_indices =
783 checking_data.shuffled_indices[a_i * samples..(a_i + 1) * samples].to_vec();
784 let lg_rows = checking_data.topology.encoded_rows.ilog2() as usize;
785 let shift = vanishing(lg_rows as u8, &a_indices);
786 let mut checksum = (*shards[1].checksum).clone();
787 for (i, shift_i) in shift.coefficients_up_to(checksum.rows()).enumerate() {
788 for j in 0..checksum.cols() {
789 checksum[(i, j)] += &shift_i[0];
790 }
791 }
792 shards[1].checksum = Arc::new(checksum);
793 shards[2].checksum = shards[1].checksum.clone();
794 }
795
796 assert!(matches!(
797 Zoda::<Sha256>::weaken(b"", &config, &commitment, b_i as u16, shards[b_i].clone()),
798 Err(Error::InvalidWeakShard)
799 ));
800
801 assert!(matches!(
804 Zoda::<Sha256>::weaken(b"", &config, &commitment, a_i as u16, shards[a_i].clone()),
805 Err(Error::InvalidWeakShard)
806 ));
807 }
808
809 #[cfg(feature = "arbitrary")]
810 mod conformance {
811 use super::*;
812 use commonware_codec::conformance::CodecConformance;
813 use commonware_conformance::Conformance;
814 use commonware_cryptography::sha256::Digest as Sha256Digest;
815
816 struct EncodeCheck;
817
818 impl Conformance for EncodeCheck {
819 async fn commit(seed: u64) -> Vec<u8> {
820 let config = Config {
821 minimum_shards: NZU16!(2),
822 extra_shards: NZU16!(1),
823 };
824 let data: Vec<_> = (0..seed as usize % 768)
825 .map(|i| (seed as u8).wrapping_add(i as u8))
826 .collect();
827
828 let (commitment, shards) =
829 Zoda::<Sha256>::encode(b"conformance", &config, &data[..], &STRATEGY).unwrap();
830
831 let mut log = commitment.encode().to_vec();
832 for (i, shard) in shards.into_iter().enumerate() {
833 let index: u16 = i.try_into().unwrap();
834 let (checking_data, _, weak_shard) =
835 Zoda::<Sha256>::weaken(b"conformance", &config, &commitment, index, shard)
836 .unwrap();
837 let checked_shard = Zoda::<Sha256>::check(
838 &config,
839 &commitment,
840 &checking_data,
841 index,
842 weak_shard.clone(),
843 )
844 .unwrap();
845
846 log.extend(index.encode());
847 log.extend(weak_shard.encode());
848 log.extend(checked_shard.shard.encode());
849 log.extend(checked_shard.commitment.encode());
850 }
851 log
852 }
853 }
854
855 commonware_conformance::conformance_tests! {
856 EncodeCheck => 256,
857 CodecConformance<StrongShard<Sha256Digest>>,
858 CodecConformance<WeakShard<Sha256Digest>>,
859 }
860 }
861}