1use super::{
8 ChunkHdr, CommitContext, DisclosedPayload, LenProof, PayloadWire, Selector, Step, VerifyError,
9 encode_disclosure, extract_bao_slice, verify_disclosure, verify_disclosure_reusing_context,
10};
11use crate::hash::{Hash, hash};
12use crate::merkle;
13use crate::object::{EntryMode, MkitError, Object};
14use crate::store::{ObjectSource, StoreError};
15use commonware_codec::{EncodeSize, Write};
16
17use super::MAX_BUNDLE_BYTES;
18
19const MAGIC: &[u8; 4] = b"MKDS";
20const VERSION: u8 = 1;
21const MAX_CHUNK_BUNDLES: usize = 1_000_000;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub struct DisclosedSpan {
27 pub commit_id: Hash,
29 pub tree_hash: Hash,
31 pub path: Vec<(Vec<u8>, EntryMode)>,
33 pub leaf_id: Hash,
35 pub signer: [u8; 32],
37 pub signature_valid: bool,
39 pub offset: u64,
41 pub bytes: Vec<u8>,
43 pub first: u32,
45 pub last: u32,
47 pub span_start: u64,
49 pub chunk_inner_root: Hash,
51}
52
53#[derive(Debug, thiserror::Error)]
55#[non_exhaustive]
56pub enum SpanError {
57 #[error("span_too_large")]
59 TooLarge,
60 #[error("span_magic")]
62 Magic,
63 #[error("span_version")]
65 Version,
66 #[error("span_encoding")]
68 Encoding,
69 #[error("span_trailing_bytes")]
71 TrailingBytes,
72 #[error("span_commit")]
74 Commit,
75 #[error("span_range_arithmetic")]
77 RangeArithmetic,
78 #[error("span_chunk_count")]
80 ChunkCount,
81 #[error("span_anchor_invalid: {0}")]
83 AnchorInvalid(#[source] VerifyError),
84 #[error("span_anchor_selector")]
86 AnchorSelector,
87 #[error("span_anchor_offset")]
89 AnchorOffset,
90 #[error("span_inner_invalid: {0}")]
92 InnerInvalid(#[source] VerifyError),
93 #[error("span_chunk_selector")]
95 ChunkSelector,
96 #[error("span_leaf_context")]
98 LeafContext,
99 #[error("span_chunk_context")]
101 ChunkContext,
102 #[error("span_chunk_order")]
104 ChunkOrder,
105 #[error("span_chunk_bytes")]
107 ChunkBytes,
108 #[error("span_anchor_binding")]
110 AnchorBinding,
111 #[error("span_range_outside")]
113 RangeOutside,
114 #[error("span_last_unneeded")]
116 LastUnneeded,
117}
118
119impl SpanError {
120 #[must_use]
122 pub const fn reason(&self) -> &'static str {
123 match self {
124 Self::TooLarge => "span_too_large",
125 Self::Magic => "span_magic",
126 Self::Version => "span_version",
127 Self::Encoding => "span_encoding",
128 Self::TrailingBytes => "span_trailing_bytes",
129 Self::Commit => "span_commit",
130 Self::RangeArithmetic => "span_range_arithmetic",
131 Self::ChunkCount => "span_chunk_count",
132 Self::AnchorInvalid(_) => "span_anchor_invalid",
133 Self::AnchorSelector => "span_anchor_selector",
134 Self::AnchorOffset => "span_anchor_offset",
135 Self::InnerInvalid(_) => "span_inner_invalid",
136 Self::ChunkSelector => "span_chunk_selector",
137 Self::LeafContext => "span_leaf_context",
138 Self::ChunkContext => "span_chunk_context",
139 Self::ChunkOrder => "span_chunk_order",
140 Self::ChunkBytes => "span_chunk_bytes",
141 Self::AnchorBinding => "span_anchor_binding",
142 Self::RangeOutside => "span_range_outside",
143 Self::LastUnneeded => "span_last_unneeded",
144 }
145 }
146}
147
148struct Reader<'a>(&'a [u8]);
149
150impl<'a> Reader<'a> {
151 fn take(&mut self, n: usize) -> Result<&'a [u8], SpanError> {
152 if n > self.0.len() {
153 return Err(SpanError::Encoding);
154 }
155 let (part, rest) = self.0.split_at(n);
156 self.0 = rest;
157 Ok(part)
158 }
159
160 fn u64(&mut self) -> Result<u64, SpanError> {
161 let bytes: [u8; 8] = self.take(8)?.try_into().map_err(|_| SpanError::Encoding)?;
162 Ok(u64::from_be_bytes(bytes))
163 }
164
165 fn length(&mut self, limit: usize) -> Result<usize, SpanError> {
167 let mut value = 0u32;
168 for i in 0..5 {
169 let byte = self.take(1)?[0];
170 if (i > 0 && byte == 0) || (i == 4 && byte > 15) {
171 return Err(SpanError::Encoding);
172 }
173 value |= u32::from(byte & 0x7f) << (7 * i);
174 if byte & 0x80 == 0 {
175 let n = value as usize;
176 return (n <= limit).then_some(n).ok_or(SpanError::Encoding);
177 }
178 }
179 Err(SpanError::Encoding)
180 }
181
182 fn vector(&mut self) -> Result<&'a [u8], SpanError> {
183 let n = self.length(self.0.len().min(MAX_BUNDLE_BYTES))?;
184 self.take(n)
185 }
186}
187
188struct Decoded<'a> {
189 commit: Hash,
190 offset: u64,
191 len: u64,
192 anchor: &'a [u8],
193 chunks: Vec<&'a [u8]>,
194}
195
196fn decode(bytes: &[u8]) -> Result<Decoded<'_>, SpanError> {
197 if bytes.len() > MAX_BUNDLE_BYTES {
198 return Err(SpanError::TooLarge);
199 }
200 let mut reader = Reader(bytes);
201 if reader.take(4)? != MAGIC {
202 return Err(SpanError::Magic);
203 }
204 if reader.take(1)? != [VERSION] {
205 return Err(SpanError::Version);
206 }
207 let commit = reader
208 .take(32)?
209 .try_into()
210 .map_err(|_| SpanError::Encoding)?;
211 let offset = reader.u64()?;
212 let len = reader.u64()?;
213 let anchor = reader.vector()?;
214 let count = reader.length(MAX_CHUNK_BUNDLES)?;
215 if count > reader.0.len() {
218 return Err(SpanError::Encoding);
219 }
220 let mut chunks = Vec::new();
221 for _ in 0..count {
222 chunks.push(reader.vector()?);
223 }
224 if !reader.0.is_empty() {
225 return Err(SpanError::TrailingBytes);
226 }
227 Ok(Decoded {
228 commit,
229 offset,
230 len,
231 anchor,
232 chunks,
233 })
234}
235
236#[must_use]
241pub fn encode_span(
242 commit: Hash,
243 offset: u64,
244 len: u64,
245 anchor: &[u8],
246 chunks: &[&[u8]],
247) -> Vec<u8> {
248 let mut out = Vec::new();
249 out.extend_from_slice(MAGIC);
250 out.push(VERSION);
251 commit.write(&mut out);
252 offset.write(&mut out);
253 len.write(&mut out);
254 anchor.write(&mut out);
255 let mut count = chunks.len();
256 loop {
257 #[allow(clippy::cast_possible_truncation)] let byte = (count & 0x7f) as u8;
259 count >>= 7;
260 out.push(if count == 0 { byte } else { byte | 0x80 });
261 if count == 0 {
262 break;
263 }
264 }
265 for chunk in chunks {
266 (*chunk).write(&mut out);
267 }
268 out
269}
270
271struct Summary {
272 selector_ok: bool,
273 leaf_ok: bool,
274 context_ok: bool,
275 index: u32,
276 content_len: Option<u64>,
277 first_byte: Option<u8>,
278 canonical_id: Hash,
279}
280
281fn check_summaries(
284 summaries: &[Summary],
285 first: u32,
286 anchor_chunk_id: &Hash,
287 anchor_byte: u8,
288) -> Result<(), SpanError> {
289 if summaries.iter().any(|s| !s.selector_ok) {
290 return Err(SpanError::ChunkSelector);
291 }
292 if summaries.iter().any(|s| !s.leaf_ok) {
293 return Err(SpanError::LeafContext);
294 }
295 if summaries.iter().any(|s| !s.context_ok) {
296 return Err(SpanError::ChunkContext);
297 }
298 if summaries
299 .iter()
300 .enumerate()
301 .any(|(i, s)| u32::try_from(i).ok().and_then(|i| first.checked_add(i)) != Some(s.index))
302 {
303 return Err(SpanError::ChunkOrder);
304 }
305 if summaries.iter().any(|s| s.content_len.is_none()) {
306 return Err(SpanError::ChunkBytes);
307 }
308 if summaries[0].canonical_id != *anchor_chunk_id || summaries[0].first_byte != Some(anchor_byte)
309 {
310 return Err(SpanError::AnchorBinding);
311 }
312 Ok(())
313}
314
315#[allow(clippy::too_many_lines)] pub fn verify_disclosure_span(trusted: &Hash, bytes: &[u8]) -> Result<DisclosedSpan, SpanError> {
323 let decoded = decode(bytes)?;
324 if &decoded.commit != trusted {
325 return Err(SpanError::Commit);
326 }
327 let end = decoded
328 .offset
329 .checked_add(decoded.len)
330 .filter(|_| decoded.len != 0)
331 .ok_or(SpanError::RangeArithmetic)?;
332 if decoded.chunks.len() < 2 {
333 return Err(SpanError::ChunkCount);
334 }
335 let anchor = verify_disclosure(trusted, decoded.anchor).map_err(SpanError::AnchorInvalid)?;
336 let DisclosedPayload::Range {
337 blob_id,
338 chunk: Some((first, total_size, chunk_size)),
339 offset_in_blob: 0,
340 absolute_offset,
341 bytes: anchor_byte,
342 } = &anchor.payload
343 else {
344 return Err(SpanError::AnchorSelector);
345 };
346 if anchor_byte.len() != 1 {
347 return Err(SpanError::AnchorSelector);
348 }
349 let start = absolute_offset.ok_or(SpanError::AnchorOffset)?;
350 let context = CommitContext::from_disclosed(&anchor);
351 let mut summaries = Vec::new();
352 let mut output = Vec::new();
353 let mut cursor = start;
354 let mut range_overflow = false;
355 let mut last_start = start;
356 for bundle in decoded.chunks {
357 let inner = verify_disclosure_reusing_context(trusted, bundle, &context)
358 .map_err(SpanError::InnerInvalid)?;
359 let leaf_ok = inner.path == anchor.path && inner.leaf_id == anchor.leaf_id;
360 let context_ok = inner.chunk_inner_root == anchor.chunk_inner_root;
361 let mut summary = Summary {
362 selector_ok: false,
363 leaf_ok,
364 context_ok,
365 index: 0,
366 content_len: None,
367 first_byte: None,
368 canonical_id: [0; 32],
369 };
370 if let DisclosedPayload::Chunk {
371 total_size: inner_total,
372 chunk_size: inner_size,
373 index,
374 bytes: chunk_bytes,
375 } = inner.payload
376 {
377 summary.selector_ok = true;
378 summary.context_ok &= inner_total == *total_size && inner_size == *chunk_size;
379 summary.index = index;
380 summary.canonical_id = hash(&chunk_bytes);
381 if let Ok(Object::Blob(blob)) = crate::serialize::deserialize(&chunk_bytes)
382 && !blob.data.is_empty()
383 {
384 summary.content_len = u64::try_from(blob.data.len()).ok();
385 summary.first_byte = blob.data.first().copied();
386 if let Some(length) = summary.content_len {
387 last_start = cursor;
388 if let Some(next) = cursor.checked_add(length) {
389 let from = decoded.offset.max(cursor);
390 let to = end.min(next);
391 if from < to {
392 let a = usize::try_from(from - cursor)
393 .map_err(|_| SpanError::RangeOutside)?;
394 let b = usize::try_from(to - cursor)
395 .map_err(|_| SpanError::RangeOutside)?;
396 output.extend_from_slice(&blob.data[a..b]);
397 }
398 cursor = next;
399 } else {
400 range_overflow = true;
401 }
402 }
403 }
404 }
405 summaries.push(summary);
406 }
407 check_summaries(&summaries, *first, blob_id, anchor_byte[0])?;
408 let first_end = start
409 .checked_add(summaries[0].content_len.ok_or(SpanError::ChunkBytes)?)
410 .ok_or(SpanError::RangeOutside)?;
411 if range_overflow
412 || cursor > *total_size
413 || decoded.offset < start
414 || decoded.offset >= first_end
415 || end > cursor
416 {
417 return Err(SpanError::RangeOutside);
418 }
419 if end <= last_start {
420 return Err(SpanError::LastUnneeded);
421 }
422 if u64::try_from(output.len()) != Ok(decoded.len) {
423 return Err(SpanError::RangeOutside);
424 }
425 let last = first
426 .checked_add(u32::try_from(summaries.len() - 1).map_err(|_| SpanError::ChunkOrder)?)
427 .ok_or(SpanError::ChunkOrder)?;
428 Ok(DisclosedSpan {
429 commit_id: *trusted,
430 tree_hash: anchor.tree_hash,
431 path: anchor.path,
432 leaf_id: anchor.leaf_id,
433 signer: anchor.signer,
434 signature_valid: anchor.signature_valid,
435 offset: decoded.offset,
436 bytes: output,
437 first: *first,
438 last,
439 span_start: start,
440 chunk_inner_root: anchor.chunk_inner_root.ok_or(SpanError::ChunkContext)?,
441 })
442}
443
444#[derive(Debug, Clone, PartialEq, Eq)]
446#[non_exhaustive]
447pub enum RangeProof {
448 Mkdp(Vec<u8>),
450 Mkds(Vec<u8>),
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456#[non_exhaustive]
457pub enum RangeProofKind {
458 Mkdp,
460 Mkds,
462}
463
464#[derive(Debug, Clone, PartialEq, Eq)]
468#[non_exhaustive]
469pub struct Plan {
470 pub kind: RangeProofKind,
472 pub first: usize,
474 pub last: usize,
476 pub needed_chunk_indices: Vec<usize>,
478}
479
480#[derive(Debug, thiserror::Error)]
482#[non_exhaustive]
483pub enum RangeProofError {
484 #[error("range length must be nonzero")]
486 ZeroLength,
487 #[error("range endpoint overflow")]
489 OffsetOverflow,
490 #[error("range is outside the file")]
492 OutOfBounds,
493 #[error("boundary hints must start at zero, strictly increase, and end at total_size")]
496 InvalidBoundaries,
497 #[error("boundary hint differs from chunk {index}'s canonical Blob length")]
499 HintMismatch {
500 index: usize,
502 },
503 #[error("chunk {index} is not a canonical nonempty Blob")]
505 InvalidChunk {
506 index: usize,
508 },
509 #[error("encoded proof exceeds 64 MiB")]
511 ProofTooLarge,
512 #[error(transparent)]
514 Store(#[from] StoreError),
515 #[error(transparent)]
517 Verify(#[from] VerifyError),
518 #[error(transparent)]
520 Decode(#[from] MkitError),
521}
522
523pub fn plan_range_proof(
530 chunk_lengths: &[u64],
531 offset: u64,
532 len: u64,
533) -> Result<Plan, RangeProofError> {
534 if len == 0 {
535 return Err(RangeProofError::ZeroLength);
536 }
537 let end = offset
538 .checked_add(len)
539 .ok_or(RangeProofError::OffsetOverflow)?;
540 let mut cursor = 0u64;
541 let mut first = None;
542 let mut last = None;
543 for (i, &length) in chunk_lengths.iter().enumerate() {
544 if length == 0 {
545 return Err(RangeProofError::InvalidChunk { index: i });
546 }
547 let next = cursor
548 .checked_add(length)
549 .ok_or(RangeProofError::OffsetOverflow)?;
550 if first.is_none() && offset < next {
551 first = Some(i);
552 }
553 if first.is_some() && end <= next {
554 last = Some(i);
555 break;
556 }
557 cursor = next;
558 }
559 let first = first.ok_or(RangeProofError::OutOfBounds)?;
560 let last = last.ok_or(RangeProofError::OutOfBounds)?;
561 Ok(Plan {
562 kind: if first == last {
563 RangeProofKind::Mkdp
564 } else {
565 RangeProofKind::Mkds
566 },
567 first,
568 last,
569 needed_chunk_indices: (0..=last).collect(),
570 })
571}
572
573fn range_payload(
574 cb: &crate::object::ChunkedBlob,
575 index: usize,
576 bytes: &[u8],
577 offset_in_blob: u64,
578 len: u64,
579 proofs: Vec<LenProof>,
580) -> Result<PayloadWire, RangeProofError> {
581 let index = u32::try_from(index).map_err(|_| VerifyError::TooManyChunks)?;
582 let position = index.checked_add(1).ok_or(VerifyError::TooManyChunks)?;
583 let chunk_id = cb.chunks[index as usize];
584 let proof = merkle::build_chunks_multi_proof(cb, [0, position]).map_err(VerifyError::from)?;
585 let bao_offset = offset_in_blob
586 .checked_add(10)
587 .ok_or(RangeProofError::OffsetOverflow)?;
588 let slice = extract_bao_slice(bytes, bao_offset, len)?;
589 Ok(PayloadWire::Range {
590 chunk: Some(ChunkHdr {
591 total_size: cb.total_size,
592 chunk_size: cb.chunk_size,
593 index,
594 inner_root: merkle::chunked_inner_root(cb),
595 chunk_id,
596 proof,
597 }),
598 offset_in_blob,
599 len,
600 slice,
601 chunk_len_proofs: proofs,
602 })
603}
604
605fn chunk_payload(
606 cb: &crate::object::ChunkedBlob,
607 index: usize,
608 bytes: Vec<u8>,
609) -> Result<PayloadWire, RangeProofError> {
610 let index = u32::try_from(index).map_err(|_| VerifyError::TooManyChunks)?;
611 let position = index.checked_add(1).ok_or(VerifyError::TooManyChunks)?;
612 Ok(PayloadWire::Chunk {
613 total_size: cb.total_size,
614 chunk_size: cb.chunk_size,
615 index,
616 inner_root: merkle::chunked_inner_root(cb),
617 proof: merkle::build_chunks_multi_proof(cb, [0, position]).map_err(VerifyError::from)?,
618 bytes,
619 })
620}
621
622fn preceding_proof(
625 cb: &crate::object::ChunkedBlob,
626 index: usize,
627 id: Hash,
628 bytes: Vec<u8>,
629) -> Result<LenProof, RangeProofError> {
630 #[cfg(test)]
631 let bytes = TrackedPrecedingBytes::new(bytes);
632 let index_u32 = u32::try_from(index).map_err(|_| VerifyError::TooManyChunks)?;
633 let position = index_u32.checked_add(1).ok_or(VerifyError::TooManyChunks)?;
634 #[cfg(test)]
635 let slice = extract_bao_slice(&bytes.0, 0, 10)?;
636 #[cfg(not(test))]
637 let slice = extract_bao_slice(&bytes, 0, 10)?;
638 drop(bytes);
639 Ok(LenProof {
640 index: index_u32,
641 chunk_id: id,
642 proof: merkle::build_chunk_proof(cb, position).map_err(VerifyError::from)?,
643 slice,
644 })
645}
646
647#[cfg(test)]
648std::thread_local! {
649 static PRECEDING_LIVE: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
650 static PRECEDING_PEAK: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
651}
652
653#[cfg(test)]
654struct TrackedPrecedingBytes(Vec<u8>);
655
656#[cfg(test)]
657impl TrackedPrecedingBytes {
658 fn new(bytes: Vec<u8>) -> Self {
659 PRECEDING_LIVE.with(|live| {
660 let count = live.get() + 1;
661 live.set(count);
662 PRECEDING_PEAK.with(|peak| peak.set(peak.get().max(count)));
663 });
664 Self(bytes)
665 }
666}
667
668#[cfg(test)]
669impl Drop for TrackedPrecedingBytes {
670 fn drop(&mut self) {
671 PRECEDING_LIVE.with(|live| live.set(live.get() - 1));
672 }
673}
674
675fn varint_size(mut n: usize) -> usize {
676 let mut size = 1;
677 while n >= 128 {
678 n >>= 7;
679 size += 1;
680 }
681 size
682}
683
684fn encoded_disclosure_size(
685 commit_bytes: &[u8],
686 steps: &[Step],
687 payload: &PayloadWire,
688) -> Result<usize, RangeProofError> {
689 let payload_size = match payload {
690 PayloadWire::Object { bytes } => bytes.as_slice().encode_size(),
691 PayloadWire::Chunk { proof, bytes, .. } => {
692 8 + 4 + 4 + 32 + proof.encode_size() + bytes.as_slice().encode_size()
693 }
694 PayloadWire::Range {
695 chunk,
696 slice,
697 chunk_len_proofs,
698 ..
699 } => {
700 chunk.encode_size()
701 + 8
702 + 8
703 + slice.as_slice().encode_size()
704 + chunk_len_proofs.encode_size()
705 }
706 };
707 4usize
708 .checked_add(1 + 32 + 1)
709 .and_then(|n| n.checked_add(commit_bytes.encode_size()))
710 .and_then(|n| n.checked_add(steps.encode_size()))
711 .and_then(|n| n.checked_add(payload_size))
712 .filter(|&n| n <= MAX_BUNDLE_BYTES)
713 .ok_or(RangeProofError::ProofTooLarge)
714}
715
716#[allow(clippy::too_many_lines)] pub fn build_range_proof_from<S: ObjectSource + ?Sized>(
728 source: &S,
729 commit_id: &Hash,
730 path: &[&[u8]],
731 offset: u64,
732 len: u64,
733 boundaries: Option<&[u64]>,
734) -> Result<RangeProof, RangeProofError> {
735 if len == 0 {
736 return Err(RangeProofError::ZeroLength);
737 }
738 let end = offset
739 .checked_add(len)
740 .ok_or(RangeProofError::OffsetOverflow)?;
741 let (commit_bytes, steps, leaf_id) = super::build_prefix(source, commit_id, path)?;
742 let leaf = source.read_object(&leaf_id)?;
743 let cb = match leaf {
744 Object::Blob(_) => {
745 if boundaries.is_some() {
746 return Err(RangeProofError::InvalidBoundaries);
747 }
748 let payload = super::build_payload(
749 source,
750 &leaf_id,
751 Selector::Range {
752 offset,
753 len,
754 with_offsets: true,
755 },
756 )?;
757 encoded_disclosure_size(&commit_bytes, &steps, &payload)?;
758 let proof = encode_disclosure(commit_id, &commit_bytes, &steps, &payload);
759 return Ok(RangeProof::Mkdp(proof));
760 }
761 Object::ChunkedBlob(cb) => cb,
762 _ => return Err(VerifyError::SelectorLeafMismatch.into()),
763 };
764 if end > cb.total_size {
765 return Err(RangeProofError::OutOfBounds);
766 }
767 let hinted_plan = if let Some(hints) = boundaries {
768 if hints.len() != cb.chunks.len() + 1
769 || hints.first() != Some(&0)
770 || hints.last() != Some(&cb.total_size)
771 || hints.windows(2).any(|pair| pair[0] >= pair[1])
772 {
773 return Err(RangeProofError::InvalidBoundaries);
774 }
775 let lengths: Vec<u64> = hints.windows(2).map(|pair| pair[1] - pair[0]).collect();
776 Some(plan_range_proof(&lengths, offset, len)?)
777 } else {
778 None
779 };
780 let mut preceding = Vec::new();
781 let mut preceding_size = 0usize;
782 let mut encoded_container_size = 0usize;
783 let mut cursor = 0u64;
784 let mut first = None;
785 let mut anchor = None;
786 let mut chunks = Vec::new();
787 for (i, id) in cb.chunks.iter().enumerate() {
788 if hinted_plan.as_ref().is_some_and(|plan| i > plan.last) {
789 break;
790 }
791 let (chunk_bytes, content_len) = checked_blob_source(source, id, i)?;
792 let next = cursor
793 .checked_add(content_len)
794 .ok_or(RangeProofError::OffsetOverflow)?;
795 if next > cb.total_size {
796 return Err(RangeProofError::OutOfBounds);
797 }
798 if let Some(hints) = boundaries
799 && (hints[i] != cursor || hints[i + 1] != next)
800 {
801 return Err(RangeProofError::HintMismatch { index: i });
802 }
803 if first.is_none() && offset >= next {
804 let proof = preceding_proof(&cb, i, *id, chunk_bytes)?;
805 preceding_size = preceding_size
806 .checked_add(proof.encode_size())
807 .filter(|&size| size <= MAX_BUNDLE_BYTES)
808 .ok_or(RangeProofError::ProofTooLarge)?;
809 preceding.push(proof);
810 cursor = next;
811 continue;
812 }
813 let first_index = *first.get_or_insert(i);
814 if i == first_index {
815 let local_offset = offset
816 .checked_sub(cursor)
817 .ok_or(RangeProofError::OffsetOverflow)?;
818 if end <= next {
819 let payload = range_payload(&cb, i, &chunk_bytes, local_offset, len, preceding)?;
820 encoded_disclosure_size(&commit_bytes, &steps, &payload)?;
821 let proof = encode_disclosure(commit_id, &commit_bytes, &steps, &payload);
822 return Ok(RangeProof::Mkdp(proof));
823 }
824 let payload =
825 range_payload(&cb, i, &chunk_bytes, 0, 1, std::mem::take(&mut preceding))?;
826 let anchor_size = encoded_disclosure_size(&commit_bytes, &steps, &payload)?;
827 encoded_container_size =
828 4 + 1 + 32 + 8 + 8 + varint_size(anchor_size) + anchor_size + 1;
829 if encoded_container_size > MAX_BUNDLE_BYTES {
830 return Err(RangeProofError::ProofTooLarge);
831 }
832 anchor = Some(encode_disclosure(
833 commit_id,
834 &commit_bytes,
835 &steps,
836 &payload,
837 ));
838 }
839 if chunk_bytes.len() > MAX_BUNDLE_BYTES {
840 return Err(RangeProofError::ProofTooLarge);
841 }
842 let payload = chunk_payload(&cb, i, chunk_bytes)?;
843 let bundle_size = encoded_disclosure_size(&commit_bytes, &steps, &payload)?;
844 let new_count = chunks.len() + 1;
845 encoded_container_size = encoded_container_size
846 .checked_add(varint_size(new_count) - varint_size(chunks.len()))
847 .and_then(|n| n.checked_add(varint_size(bundle_size)))
848 .and_then(|n| n.checked_add(bundle_size))
849 .filter(|&n| n <= MAX_BUNDLE_BYTES)
850 .ok_or(RangeProofError::ProofTooLarge)?;
851 let bundle = encode_disclosure(commit_id, &commit_bytes, &steps, &payload);
852 chunks.push(bundle);
853 cursor = next;
854 if end <= next {
855 break;
856 }
857 }
858 let anchor = anchor.ok_or(RangeProofError::OutOfBounds)?;
859 if cursor < end {
860 return Err(RangeProofError::OutOfBounds);
861 }
862 let refs: Vec<&[u8]> = chunks.iter().map(Vec::as_slice).collect();
863 let proof = encode_span(*commit_id, offset, len, &anchor, &refs);
864 if proof.len() > MAX_BUNDLE_BYTES {
865 return Err(RangeProofError::ProofTooLarge);
866 }
867 Ok(RangeProof::Mkds(proof))
868}
869
870fn checked_blob_source<S: ObjectSource + ?Sized>(
871 source: &S,
872 id: &Hash,
873 index: usize,
874) -> Result<(Vec<u8>, u64), RangeProofError> {
875 let bytes = source.read(id)?;
876 let Object::Blob(blob) = crate::serialize::deserialize(&bytes)? else {
877 return Err(RangeProofError::InvalidChunk { index });
878 };
879 let len = u64::try_from(blob.data.len()).map_err(|_| RangeProofError::OffsetOverflow)?;
880 if len == 0 {
881 return Err(RangeProofError::InvalidChunk { index });
882 }
883 Ok((bytes, len))
884}
885
886#[cfg(test)]
887#[allow(clippy::unwrap_used)]
888mod tests {
889 use super::*;
890 use crate::hash::ZERO;
891 use crate::layout::RepoLayout;
892 use crate::object::{Blob, ChunkedBlob, Commit, Identity, Tree, TreeEntry};
893 use crate::sign::{KeyPair, sign_commit};
894 use crate::store::{ObjectStore, StoreResult};
895 use std::cell::RefCell;
896
897 const VALID: &[u8] =
898 include_bytes!("../../../../tests/golden/http-objects/span_two_chunks.bin");
899 const FIRST_ZERO: &[u8] =
900 include_bytes!("../../../../tests/golden/http-objects/span_first_zero.bin");
901
902 fn valid() -> (Hash, Decoded<'static>) {
903 let decoded = decode(VALID).unwrap();
904 (decoded.commit, decoded)
905 }
906
907 fn verify(bytes: &[u8]) -> &'static str {
908 let (trusted, _) = valid();
909 verify_disclosure_span(&trusted, bytes)
910 .unwrap_err()
911 .reason()
912 }
913
914 #[test]
915 fn structural_checks_precede_crypto_and_follow_table_order() {
916 let (trusted, d) = valid();
917 let mut trailing_and_bad_commit = VALID.to_vec();
918 trailing_and_bad_commit[5] ^= 1;
919 trailing_and_bad_commit.push(0);
920 assert_eq!(verify(&trailing_and_bad_commit), "span_trailing_bytes");
921
922 let mut wrong_magic = VALID.to_vec();
923 wrong_magic[0] = b'X';
924 wrong_magic[4] = 2;
925 assert_eq!(verify(&wrong_magic), "span_magic");
926 assert_eq!(verify(&VALID[..4]), "span_encoding");
927 assert_eq!(verify(&[b'M', b'K', b'D', b'S', 2]), "span_version");
928
929 let mut wrong_commit = VALID.to_vec();
930 wrong_commit[5] ^= 1;
931 assert_eq!(verify(&wrong_commit), "span_commit");
932 assert_eq!(
933 verify(&encode_span(trusted, d.offset, 0, d.anchor, &d.chunks)),
934 "span_range_arithmetic"
935 );
936 assert_eq!(
937 verify(&encode_span(trusted, u64::MAX, 2, d.anchor, &d.chunks)),
938 "span_range_arithmetic"
939 );
940 assert_eq!(
941 verify(&encode_span(
942 trusted,
943 d.offset,
944 d.len,
945 d.anchor,
946 &d.chunks[..1]
947 )),
948 "span_chunk_count"
949 );
950 assert_eq!(
951 verify(&encode_span(trusted, d.offset, d.len, &[], &d.chunks)),
952 "span_anchor_invalid"
953 );
954 }
955
956 #[test]
957 fn every_spec_reason_has_its_own_stable_label() {
958 let reasons = [
959 SpanError::TooLarge,
960 SpanError::Magic,
961 SpanError::Version,
962 SpanError::Encoding,
963 SpanError::TrailingBytes,
964 SpanError::Commit,
965 SpanError::RangeArithmetic,
966 SpanError::ChunkCount,
967 SpanError::AnchorInvalid(VerifyError::BadMagic),
968 SpanError::AnchorSelector,
969 SpanError::AnchorOffset,
970 SpanError::InnerInvalid(VerifyError::BadMagic),
971 SpanError::ChunkSelector,
972 SpanError::LeafContext,
973 SpanError::ChunkContext,
974 SpanError::ChunkOrder,
975 SpanError::ChunkBytes,
976 SpanError::AnchorBinding,
977 SpanError::RangeOutside,
978 SpanError::LastUnneeded,
979 ];
980 let labels = [
981 "span_too_large",
982 "span_magic",
983 "span_version",
984 "span_encoding",
985 "span_trailing_bytes",
986 "span_commit",
987 "span_range_arithmetic",
988 "span_chunk_count",
989 "span_anchor_invalid",
990 "span_anchor_selector",
991 "span_anchor_offset",
992 "span_inner_invalid",
993 "span_chunk_selector",
994 "span_leaf_context",
995 "span_chunk_context",
996 "span_chunk_order",
997 "span_chunk_bytes",
998 "span_anchor_binding",
999 "span_range_outside",
1000 "span_last_unneeded",
1001 ];
1002 for (error, label) in reasons.into_iter().zip(labels) {
1003 assert_eq!(error.reason(), label);
1004 }
1005 }
1006
1007 #[test]
1008 fn cross_bundle_reasons_include_defence_in_depth() {
1009 let id = [7; 32];
1010 let good = || Summary {
1011 selector_ok: true,
1012 leaf_ok: true,
1013 context_ok: true,
1014 index: 4,
1015 content_len: Some(5),
1016 first_byte: Some(42),
1017 canonical_id: id,
1018 };
1019 let mut cases = vec![good(), good()];
1020 cases[1].index = 5;
1021 assert!(check_summaries(&cases, 4, &id, 42).is_ok());
1022
1023 cases[1].selector_ok = false;
1024 cases[0].leaf_ok = false;
1025 assert_eq!(
1026 check_summaries(&cases, 4, &id, 42).unwrap_err().reason(),
1027 "span_chunk_selector"
1028 );
1029 cases[1].selector_ok = true;
1030 assert_eq!(
1031 check_summaries(&cases, 4, &id, 42).unwrap_err().reason(),
1032 "span_leaf_context"
1033 );
1034 cases[0].leaf_ok = true;
1035 cases[0].context_ok = false;
1036 assert_eq!(
1037 check_summaries(&cases, 4, &id, 42).unwrap_err().reason(),
1038 "span_chunk_context"
1039 );
1040 cases[0].context_ok = true;
1041 cases[1].index = 6;
1042 assert_eq!(
1043 check_summaries(&cases, 4, &id, 42).unwrap_err().reason(),
1044 "span_chunk_order"
1045 );
1046 cases[1].index = 5;
1047 cases[1].content_len = None;
1048 assert_eq!(
1049 check_summaries(&cases, 4, &id, 42).unwrap_err().reason(),
1050 "span_chunk_bytes"
1051 );
1052 cases[1].content_len = Some(5);
1053 cases[0].canonical_id = [8; 32];
1054 assert_eq!(
1055 check_summaries(&cases, 4, &id, 42).unwrap_err().reason(),
1056 "span_anchor_binding"
1057 );
1058 }
1059
1060 #[test]
1061 fn verified_bundle_failures_follow_selector_and_offset_checks() {
1062 let (trusted, d) = valid();
1063 let wrong_anchor = encode_span(trusted, d.offset, d.len, d.chunks[0], &d.chunks);
1064 assert_eq!(verify(&wrong_anchor), "span_anchor_selector");
1065
1066 let (id, commit_bytes, steps, payload) = super::super::decode_disclosure(d.anchor).unwrap();
1067 let PayloadWire::Range {
1068 chunk,
1069 offset_in_blob,
1070 len,
1071 slice,
1072 ..
1073 } = payload
1074 else {
1075 panic!("anchor Range")
1076 };
1077 let no_offsets = encode_disclosure(
1078 &id,
1079 &commit_bytes,
1080 &steps,
1081 &PayloadWire::Range {
1082 chunk,
1083 offset_in_blob,
1084 len,
1085 slice,
1086 chunk_len_proofs: Vec::new(),
1087 },
1088 );
1089 assert!(verify_disclosure(&trusted, &no_offsets).is_ok());
1090 let missing_offset = encode_span(trusted, d.offset, d.len, &no_offsets, &d.chunks);
1091 assert_eq!(verify(&missing_offset), "span_anchor_offset");
1092
1093 let bad_inner = encode_span(trusted, d.offset, d.len, d.anchor, &[b"MKDS", d.chunks[1]]);
1094 assert_eq!(verify(&bad_inner), "span_inner_invalid");
1095 let wrong_selector =
1096 encode_span(trusted, d.offset, d.len, d.anchor, &[d.anchor, d.chunks[1]]);
1097 assert_eq!(verify(&wrong_selector), "span_chunk_selector");
1098
1099 let start = verify_disclosure_span(&trusted, VALID).unwrap().span_start;
1100 let outside = encode_span(trusted, start - 1, d.len, d.anchor, &d.chunks);
1101 assert_eq!(verify(&outside), "span_range_outside");
1102 }
1103
1104 #[test]
1105 fn strict_varints_and_vector_bounds() {
1106 let (_, d) = valid();
1107 let anchor_len_at = 4 + 1 + 32 + 8 + 8;
1108 let mut nonminimal = VALID[..anchor_len_at].to_vec();
1109 nonminimal.extend_from_slice(&[0x80, 0]);
1110 nonminimal.extend_from_slice(&VALID[anchor_len_at + 2..]);
1111 assert_eq!(verify(&nonminimal), "span_encoding");
1112
1113 let mut over_u32 = VALID[..anchor_len_at].to_vec();
1114 over_u32.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0x10]);
1115 assert_eq!(verify(&over_u32), "span_encoding");
1116
1117 let mut length_over_remaining = VALID[..anchor_len_at].to_vec();
1118 length_over_remaining.extend_from_slice(&[0xff, 0xff, 0x03]);
1119 assert_eq!(verify(&length_over_remaining), "span_encoding");
1120
1121 let mut count_over_remaining = encode_span(d.commit, d.offset, d.len, d.anchor, &[]);
1122 *count_over_remaining.last_mut().unwrap() = 2;
1123 assert_eq!(verify(&count_over_remaining), "span_encoding");
1124 }
1125
1126 #[test]
1127 fn exact_boundaries_and_last_chunk_needed() {
1128 let d = decode(FIRST_ZERO).unwrap();
1129 let first = verify_disclosure(&d.commit, d.chunks[0]).unwrap();
1130 let DisclosedPayload::Chunk { bytes, .. } = first.payload else {
1131 panic!("chunk")
1132 };
1133 let Object::Blob(blob) = crate::serialize::deserialize(&bytes).unwrap() else {
1134 panic!("blob")
1135 };
1136 let edge = blob.data.len() as u64;
1137 let prior_only = encode_span(d.commit, edge - 1, 1, d.anchor, &d.chunks);
1138 assert_eq!(verify(&prior_only), "span_last_unneeded");
1139 let at_edge = encode_span(d.commit, edge - 1, 2, d.anchor, &d.chunks);
1140 assert_eq!(
1141 verify_disclosure_span(&d.commit, &at_edge)
1142 .unwrap()
1143 .bytes
1144 .len(),
1145 2
1146 );
1147 let last = verify_disclosure(&d.commit, d.chunks[1]).unwrap();
1148 let DisclosedPayload::Chunk { bytes, .. } = last.payload else {
1149 panic!("chunk")
1150 };
1151 let Object::Blob(blob) = crate::serialize::deserialize(&bytes).unwrap() else {
1152 panic!("blob")
1153 };
1154 let to_span_end = encode_span(
1155 d.commit,
1156 edge - 1,
1157 blob.data.len() as u64 + 1,
1158 d.anchor,
1159 &d.chunks,
1160 );
1161 assert_eq!(
1162 verify_disclosure_span(&d.commit, &to_span_end)
1163 .unwrap()
1164 .bytes
1165 .len() as u64,
1166 blob.data.len() as u64 + 1
1167 );
1168 }
1169
1170 #[test]
1171 fn pure_plan_respects_exact_edges() {
1172 let one = plan_range_proof(&[10, 20, 30], 5, 5).unwrap();
1173 assert_eq!(
1174 (one.kind, one.first, one.last),
1175 (RangeProofKind::Mkdp, 0, 0)
1176 );
1177 let two = plan_range_proof(&[10, 20, 30], 9, 2).unwrap();
1178 assert_eq!(
1179 (two.kind, two.first, two.last),
1180 (RangeProofKind::Mkds, 0, 1)
1181 );
1182 let later = plan_range_proof(&[10, 20, 30], 12, 18).unwrap();
1183 assert_eq!(
1184 (later.kind, later.first, later.last),
1185 (RangeProofKind::Mkdp, 1, 1)
1186 );
1187 assert_eq!(later.needed_chunk_indices, vec![0, 1]);
1188 assert!(matches!(
1189 plan_range_proof(&[10], 0, 0),
1190 Err(RangeProofError::ZeroLength)
1191 ));
1192 }
1193
1194 struct CountingSource<'a> {
1195 store: &'a ObjectStore,
1196 reads: RefCell<Vec<Hash>>,
1197 }
1198
1199 impl ObjectSource for CountingSource<'_> {
1200 fn read(&self, h: &Hash) -> StoreResult<Vec<u8>> {
1201 PRECEDING_LIVE.with(|live| assert_eq!(live.get(), 0));
1202 self.reads.borrow_mut().push(*h);
1203 self.store.read(h)
1204 }
1205 }
1206
1207 #[test]
1208 fn builder_reads_preceding_once_and_never_reads_after_span() {
1209 let temp = tempfile::TempDir::new().unwrap();
1210 let store = ObjectStore::init(&RepoLayout::single(temp.path())).unwrap();
1211 let mut chunk_ids = Vec::new();
1212 for i in 0u8..4 {
1213 let bytes = crate::serialize::serialize(&Object::Blob(Blob {
1214 data: vec![i + 1; 2048],
1215 }))
1216 .unwrap();
1217 chunk_ids.push(store.write(&bytes).unwrap());
1218 }
1219 let manifest = ChunkedBlob {
1220 total_size: 8192,
1221 chunk_size: 2048,
1222 chunks: chunk_ids.clone(),
1223 };
1224 let leaf_id = store
1225 .write(&crate::serialize::serialize(&Object::ChunkedBlob(manifest)).unwrap())
1226 .unwrap();
1227 let tree = Tree {
1228 entries: vec![TreeEntry {
1229 name: b"file".to_vec(),
1230 mode: EntryMode::Blob,
1231 object_hash: leaf_id,
1232 }],
1233 };
1234 let tree_hash = store
1235 .write(&crate::serialize::serialize(&Object::Tree(tree)).unwrap())
1236 .unwrap();
1237 let kp = KeyPair::from_seed([3; 32]);
1238 let mut commit = Commit {
1239 tree_hash,
1240 parents: vec![],
1241 author: Identity::ed25519(kp.public.0),
1242 signer: kp.public.0,
1243 message: b"span unit".to_vec(),
1244 timestamp: 1,
1245 message_hash: ZERO,
1246 content_digest: ZERO,
1247 signature: [0; 64],
1248 };
1249 commit.signature = sign_commit(&commit, &kp).unwrap().0;
1250 let commit_id = store
1251 .write(&crate::serialize::serialize(&Object::Commit(commit)).unwrap())
1252 .unwrap();
1253 let source = CountingSource {
1254 store: &store,
1255 reads: RefCell::new(Vec::new()),
1256 };
1257 PRECEDING_PEAK.with(|peak| peak.set(0));
1258 let RangeProof::Mkds(proof) =
1259 build_range_proof_from(&source, &commit_id, &[b"file"], 2047, 2, None).unwrap()
1260 else {
1261 panic!("expected MKDS")
1262 };
1263 assert_eq!(
1264 verify_disclosure_span(&commit_id, &proof).unwrap().bytes,
1265 [1, 2]
1266 );
1267 let reads = source.reads.borrow();
1268 assert_eq!(reads.iter().filter(|h| **h == chunk_ids[0]).count(), 1);
1269 assert_eq!(reads.iter().filter(|h| **h == chunk_ids[1]).count(), 1);
1270 assert!(!reads.contains(&chunk_ids[2]));
1271 assert!(!reads.contains(&chunk_ids[3]));
1272 drop(reads);
1273 PRECEDING_LIVE.with(|live| assert_eq!(live.get(), 0));
1274 PRECEDING_PEAK.with(|peak| peak.set(0));
1277 assert!(matches!(
1278 build_range_proof_from(&source, &commit_id, &[b"file"], 6200, 1, None).unwrap(),
1279 RangeProof::Mkdp(_)
1280 ));
1281 PRECEDING_PEAK.with(|peak| assert_eq!(peak.get(), 1));
1282 assert!(matches!(
1283 build_range_proof_from(
1284 &source,
1285 &commit_id,
1286 &[b"file"],
1287 2047,
1288 2,
1289 Some(&[0, 2000, 4048, 6096, 8192])
1290 ),
1291 Err(RangeProofError::HintMismatch { index: 0 })
1292 ));
1293 }
1294 #[test]
1295 fn inner_commit_bytes_must_hash_to_the_trusted_id() {
1296 let (trusted, d) = valid();
1297 let (id, commit_bytes, steps, payload) =
1298 super::super::decode_disclosure(d.chunks[1]).unwrap();
1299 let mut altered = commit_bytes.clone();
1300 altered.push(0);
1301 let forged = encode_disclosure(&id, &altered, &steps, &payload);
1302 let container = encode_span(trusted, d.offset, d.len, d.anchor, &[d.chunks[0], &forged]);
1303 let error = verify_disclosure_span(&trusted, &container).unwrap_err();
1304 assert_eq!(error.reason(), "span_inner_invalid");
1305 assert!(matches!(
1306 error,
1307 SpanError::InnerInvalid(VerifyError::CommitBytesHashMismatch)
1308 ));
1309 }
1310
1311 #[test]
1312 fn invalid_boundary_hints_are_typed() {
1313 let temp = tempfile::TempDir::new().unwrap();
1314 let store = ObjectStore::init(&RepoLayout::single(temp.path())).unwrap();
1315 let chunk = |b: u8| {
1316 store
1317 .write(
1318 &crate::serialize::serialize(&Object::Blob(Blob { data: vec![b; 10] }))
1319 .unwrap(),
1320 )
1321 .unwrap()
1322 };
1323 let cb = ChunkedBlob {
1324 total_size: 20,
1325 chunk_size: 10,
1326 chunks: vec![chunk(1), chunk(2)],
1327 };
1328 let leaf = store
1329 .write(&crate::serialize::serialize(&Object::ChunkedBlob(cb)).unwrap())
1330 .unwrap();
1331 let blob = store
1332 .write(&crate::serialize::serialize(&Object::Blob(Blob { data: vec![9; 10] })).unwrap())
1333 .unwrap();
1334 let tree = Tree {
1335 entries: vec![
1336 TreeEntry {
1337 name: b"blob".to_vec(),
1338 mode: EntryMode::Blob,
1339 object_hash: blob,
1340 },
1341 TreeEntry {
1342 name: b"chunked".to_vec(),
1343 mode: EntryMode::Blob,
1344 object_hash: leaf,
1345 },
1346 ],
1347 };
1348 let tree_hash = store
1349 .write(&crate::serialize::serialize(&Object::Tree(tree)).unwrap())
1350 .unwrap();
1351 let kp = KeyPair::from_seed([4; 32]);
1352 let mut commit = Commit {
1353 tree_hash,
1354 parents: vec![],
1355 author: Identity::ed25519(kp.public.0),
1356 signer: kp.public.0,
1357 message: b"hints".to_vec(),
1358 timestamp: 1,
1359 message_hash: ZERO,
1360 content_digest: ZERO,
1361 signature: [0; 64],
1362 };
1363 commit.signature = sign_commit(&commit, &kp).unwrap().0;
1364 let commit_id = store
1365 .write(&crate::serialize::serialize(&Object::Commit(commit)).unwrap())
1366 .unwrap();
1367 for bad in [
1368 &[0u64, 20][..],
1369 &[1, 10, 20],
1370 &[0, 10, 19],
1371 &[0, 10, 10],
1372 &[0, 20, 10],
1373 ] {
1374 assert!(
1375 matches!(
1376 build_range_proof_from(&store, &commit_id, &[b"chunked"], 0, 1, Some(bad)),
1377 Err(RangeProofError::InvalidBoundaries)
1378 ),
1379 "{bad:?}"
1380 );
1381 }
1382 assert!(matches!(
1384 build_range_proof_from(&store, &commit_id, &[b"blob"], 0, 1, Some(&[0, 10])),
1385 Err(RangeProofError::InvalidBoundaries)
1386 ));
1387 }
1388 #[test]
1389 fn span_goldens_have_exact_metadata_planned_lengths() {
1390 use super::super::proof_size::{PrefixStep, chunked_range_proof_size};
1391 for bytes in [
1392 include_bytes!("../../../../tests/golden/http-objects/span_two_chunks.bin").as_slice(),
1393 include_bytes!("../../../../tests/golden/http-objects/span_three_chunks.bin")
1394 .as_slice(),
1395 include_bytes!("../../../../tests/golden/http-objects/span_first_zero.bin").as_slice(),
1396 ] {
1397 let span = decode(bytes).unwrap();
1398 let (_, commit, steps, payload) = super::super::decode_disclosure(span.anchor).unwrap();
1399 let PayloadWire::Range {
1400 chunk: Some(chunk),
1401 chunk_len_proofs,
1402 ..
1403 } = payload
1404 else {
1405 panic!("anchor")
1406 };
1407 let mut lengths: Vec<u64> = chunk_len_proofs
1408 .iter()
1409 .map(|proof| u64::from_le_bytes(proof.slice[..8].try_into().unwrap()) - 10)
1410 .collect();
1411 for bundle in span.chunks {
1412 let (_, _, _, PayloadWire::Chunk { bytes, .. }) =
1413 super::super::decode_disclosure(bundle).unwrap()
1414 else {
1415 panic!("chunk")
1416 };
1417 lengths.push(bytes.len() as u64 - 10);
1418 }
1419 let metadata: Vec<PrefixStep> = steps
1420 .iter()
1421 .map(|step| PrefixStep {
1422 name_len: step.name.len(),
1423 position: step.position,
1424 leaf_count: step.proof.leaf_count,
1425 })
1426 .collect();
1427 let plan = chunked_range_proof_size(
1428 commit.len() as u64,
1429 &metadata,
1430 chunk.proof.leaf_count - 1,
1431 &lengths,
1432 span.offset,
1433 span.len,
1434 )
1435 .unwrap();
1436 assert_eq!(plan.kind, RangeProofKind::Mkds);
1437 assert_eq!(plan.encoded_size, bytes.len() as u64);
1438 }
1439 }
1440}