1use super::span::{RangeProofError, RangeProofKind};
6use super::{MAX_BUNDLE_BYTES, MAX_COMMIT_BYTES, VerifyError};
7use crate::merkle;
8
9#[derive(Debug, Clone, Copy)]
11pub struct PrefixStep {
12 pub name_len: usize,
14 pub position: u32,
16 pub leaf_count: u32,
18}
19
20#[derive(Debug, Clone, Copy)]
22pub struct EncodedRangePlan {
23 pub kind: RangeProofKind,
25 pub first: usize,
27 pub last: usize,
29 pub encoded_size: u64,
31}
32
33fn varint_size(mut value: u64) -> u128 {
34 let mut size = 1;
35 while value >= 128 {
36 value >>= 7;
37 size += 1;
38 }
39 size
40}
41
42fn vector_size(bytes: u64) -> u128 {
43 varint_size(bytes) + u128::from(bytes)
44}
45
46fn capped(size: u128) -> Result<u64, RangeProofError> {
47 if size > MAX_BUNDLE_BYTES as u128 {
48 Err(RangeProofError::ProofTooLarge)
49 } else {
50 u64::try_from(size).map_err(|_| RangeProofError::ProofTooLarge)
51 }
52}
53
54fn proof_size(count: u32, positions: &[u32]) -> Result<u128, RangeProofError> {
55 merkle::proof_encoded_size(count, positions.iter().copied())
56 .map(|size| size as u128)
57 .map_err(|error| VerifyError::from(error).into())
58}
59
60fn prefix_size(commit_bytes: u64, steps: &[PrefixStep]) -> Result<u128, RangeProofError> {
61 if commit_bytes > MAX_COMMIT_BYTES as u64 {
62 return Err(RangeProofError::ProofTooLarge);
63 }
64 if steps.len() > crate::store::MAX_TREE_DEPTH {
65 return Err(VerifyError::TooManySteps(steps.len()).into());
66 }
67 let mut size = 38 + vector_size(commit_bytes) + varint_size(steps.len() as u64);
68 for step in steps {
69 if step.name_len == 0 || step.name_len > 255 {
70 return Err(VerifyError::Malformed.into());
71 }
72 size +=
73 vector_size(step.name_len as u64) + 69 + proof_size(step.leaf_count, &[step.position])?;
74 }
75 capped(size)?;
76 Ok(size)
77}
78
79pub fn object_proof_size(
85 commit_bytes: u64,
86 steps: &[PrefixStep],
87 object_bytes: u64,
88) -> Result<u64, RangeProofError> {
89 capped(prefix_size(commit_bytes, steps)? + vector_size(object_bytes))
90}
91
92fn bao_parents(base: u64, leaves: u64, first: u64, last: u64) -> u64 {
96 if first <= base && base + leaves - 1 <= last {
97 return leaves - 1;
98 }
99 if leaves == 1 || last < base || first >= base + leaves {
100 return 0;
101 }
102 let left = 1u64 << (leaves - 1).ilog2();
103 1 + bao_parents(base, left, first, last) + bao_parents(base + left, leaves - left, first, last)
104}
105
106fn bao_slice_size(canonical_bytes: u64, offset: u64, len: u64) -> Result<u64, RangeProofError> {
107 if len == 0 {
108 return Err(RangeProofError::ZeroLength);
109 }
110 let end = offset
111 .checked_add(len)
112 .ok_or(RangeProofError::OffsetOverflow)?;
113 if end > canonical_bytes {
114 return Err(RangeProofError::OutOfBounds);
115 }
116 let first = offset / 1024;
117 let last = (end - 1) / 1024;
118 let covered_end = u128::from(last + 1) * 1024;
119 let covered = covered_end.min(u128::from(canonical_bytes)) - u128::from(first) * 1024;
120 let parents = bao_parents(0, canonical_bytes.div_ceil(1024), first, last);
121 capped(8 + covered + u128::from(parents) * 64)
122}
123
124pub fn blob_range_proof_size(
129 commit_bytes: u64,
130 steps: &[PrefixStep],
131 canonical_blob_bytes: u64,
132 offset: u64,
133 len: u64,
134) -> Result<u64, RangeProofError> {
135 let bao_offset = offset
136 .checked_add(10)
137 .ok_or(RangeProofError::OffsetOverflow)?;
138 let slice = bao_slice_size(canonical_blob_bytes, bao_offset, len)?;
139 capped(prefix_size(commit_bytes, steps)? + 1 + 16 + vector_size(slice) + 1)
140}
141
142fn canonical_length(content: u64, index: usize) -> Result<u64, RangeProofError> {
143 if content == 0 {
144 return Err(RangeProofError::InvalidChunk { index });
145 }
146 content
147 .checked_add(10)
148 .ok_or(RangeProofError::OffsetOverflow)
149}
150
151pub fn chunked_range_proof_size(
160 commit_bytes: u64,
161 steps: &[PrefixStep],
162 total_chunks: u32,
163 chunk_content_lengths: &[u64],
164 offset: u64,
165 len: u64,
166) -> Result<EncodedRangePlan, RangeProofError> {
167 let mut sizer = ChunkedRangeSizer::new(
168 commit_bytes,
169 steps,
170 total_chunks,
171 offset,
172 len,
173 MAX_BUNDLE_BYTES as u64,
174 )?;
175 for &content in chunk_content_lengths {
176 if let Some(plan) = sizer.push(content)? {
177 return Ok(plan);
178 }
179 }
180 Err(RangeProofError::OutOfBounds)
181}
182
183#[derive(Debug)]
187pub struct ChunkedRangeSizer {
188 prefix: u128,
189 count: u32,
190 offset: u64,
191 end: u64,
192 cap: u64,
193 preceding: u128,
194 cursor: u64,
195 index: usize,
196 first: Option<usize>,
197 container: u128,
198 finished: bool,
199}
200
201impl ChunkedRangeSizer {
202 pub fn new(
207 commit_bytes: u64,
208 steps: &[PrefixStep],
209 total_chunks: u32,
210 offset: u64,
211 len: u64,
212 max_encoded_size: u64,
213 ) -> Result<Self, RangeProofError> {
214 if len == 0 {
215 return Err(RangeProofError::ZeroLength);
216 }
217 let end = offset
218 .checked_add(len)
219 .ok_or(RangeProofError::OffsetOverflow)?;
220 if total_chunks > crate::serialize::MAX_CHUNKS {
221 return Err(VerifyError::TooManyChunks.into());
222 }
223 let mut sizer = Self {
224 prefix: prefix_size(commit_bytes, steps)?,
225 count: total_chunks
226 .checked_add(1)
227 .ok_or(VerifyError::TooManyChunks)?,
228 offset,
229 end,
230 cap: max_encoded_size.min(MAX_BUNDLE_BYTES as u64),
231 preceding: 0,
232 cursor: 0,
233 index: 0,
234 first: None,
235 container: 0,
236 finished: false,
237 };
238 sizer.check(sizer.prefix)?;
239 sizer.finished = total_chunks == 0;
241 Ok(sizer)
242 }
243
244 fn check(&self, size: u128) -> Result<u64, RangeProofError> {
245 let size = capped(size)?;
246 if size > self.cap {
247 Err(RangeProofError::ProofTooLarge)
248 } else {
249 Ok(size)
250 }
251 }
252
253 #[allow(clippy::too_many_lines)] pub fn push(&mut self, content: u64) -> Result<Option<EncodedRangePlan>, RangeProofError> {
260 let index = self.index;
261 let position = u32::try_from(index)
262 .ok()
263 .and_then(|n| n.checked_add(1))
264 .filter(|&n| n < self.count && !self.finished)
265 .ok_or(RangeProofError::OutOfBounds)?;
266 let canonical = canonical_length(content, index)?;
267 let next = self
268 .cursor
269 .checked_add(content)
270 .ok_or(RangeProofError::OffsetOverflow)?;
271 let plan = if self.first.is_none() && self.offset >= next {
272 let slice = bao_slice_size(canonical, 0, 10)?;
273 self.preceding += 36 + proof_size(self.count, &[position])? + vector_size(slice);
274 self.check(self.prefix + self.preceding)?;
275 None
276 } else {
277 let first_index = *self.first.get_or_insert(index);
278 let chunk_proof = proof_size(self.count, &[0, position])?;
279 if index == first_index {
280 let single = self.end <= next;
281 let (local_offset, selected_len) = if single {
282 (self.offset - self.cursor, self.end - self.offset)
283 } else {
284 (0, 1)
285 };
286 let slice = bao_slice_size(canonical, local_offset + 10, selected_len)?;
287 let range_size = self.check(
288 self.prefix
289 + 81
290 + chunk_proof
291 + 16
292 + vector_size(slice)
293 + varint_size(first_index as u64)
294 + self.preceding,
295 )?;
296 if single {
297 self.finished = true;
298 return Ok(Some(EncodedRangePlan {
299 kind: RangeProofKind::Mkdp,
300 first: index,
301 last: index,
302 encoded_size: range_size,
303 }));
304 }
305 self.container = 53 + vector_size(range_size);
306 }
307 let chunk_size = self.check(self.prefix + 48 + chunk_proof + vector_size(canonical))?;
308 self.container += vector_size(chunk_size);
309 let encoded_size =
310 self.check(self.container + varint_size((index - first_index + 1) as u64))?;
311 (self.end <= next).then_some(EncodedRangePlan {
312 kind: RangeProofKind::Mkds,
313 first: first_index,
314 last: index,
315 encoded_size,
316 })
317 };
318 self.cursor = next;
319 self.index += 1;
320 self.finished = plan.is_some();
321 Ok(plan)
322 }
323}
324
325#[cfg(test)]
326#[allow(clippy::unwrap_used, clippy::cast_possible_truncation)]
327mod tests {
328 use super::*;
329 use crate::object::ChunkedBlob;
330 use commonware_codec::EncodeSize;
331
332 #[test]
333 fn bao_sizing_matches_actual_extractors_at_chunk_and_tree_edges() {
334 for canonical in [10, 11, 1023, 1024, 1025, 2048, 2049, 4097, 8193, 16383] {
335 let bytes = vec![42; canonical];
336 for offset in [0, 1, 9, 10, 1023, 1024, 2047, 2048, canonical - 1] {
337 if offset >= canonical {
338 continue;
339 }
340 for len in [1, 10, 1024, canonical - offset] {
341 if offset + len > canonical {
342 continue;
343 }
344 let actual =
345 super::super::extract_bao_slice(&bytes, offset as u64, len as u64).unwrap();
346 assert_eq!(
347 bao_slice_size(canonical as u64, offset as u64, len as u64).unwrap(),
348 actual.len() as u64,
349 "canonical={canonical}, offset={offset}, len={len}"
350 );
351 }
352 }
353 }
354 }
355
356 #[test]
357 fn merkle_sizes_match_odd_even_and_power_of_two_shapes() {
358 for chunks in 1u32..=70 {
359 let manifest = ChunkedBlob {
360 total_size: u64::from(chunks),
361 chunk_size: 0,
362 chunks: (0..chunks).map(|n| [n as u8; 32]).collect(),
363 };
364 for index in 0..chunks {
365 let position = index + 1;
366 assert_eq!(
367 proof_size(chunks + 1, &[position]).unwrap(),
368 merkle::build_chunk_proof(&manifest, position)
369 .unwrap()
370 .encode_size() as u128
371 );
372 assert_eq!(
373 proof_size(chunks + 1, &[0, position]).unwrap(),
374 merkle::build_chunks_multi_proof(&manifest, [0, position])
375 .unwrap()
376 .encode_size() as u128
377 );
378 }
379 }
380 }
381
382 fn metadata(steps: &[super::super::Step]) -> Vec<PrefixStep> {
383 steps
384 .iter()
385 .map(|step| PrefixStep {
386 name_len: step.name.len(),
387 position: step.position,
388 leaf_count: step.proof.leaf_count,
389 })
390 .collect()
391 }
392
393 fn canonical_slice_length(slice: &[u8]) -> u64 {
394 u64::from_le_bytes(slice[..8].try_into().unwrap())
395 }
396
397 #[test]
398 fn canonical_object_and_range_goldens_have_exact_planned_lengths() {
399 let goldens: &[&[u8]] = &[
400 include_bytes!("../../../../tests/golden/disclosure/root_tree.bin"),
401 include_bytes!("../../../../tests/golden/disclosure/shallow_file.bin"),
402 include_bytes!("../../../../tests/golden/disclosure/nested_file_3levels.bin"),
403 include_bytes!("../../../../tests/golden/disclosure/small_blob_range_whole.bin"),
404 include_bytes!("../../../../tests/golden/disclosure/small_blob_range_first_block.bin"),
405 include_bytes!(
406 "../../../../tests/golden/disclosure/small_blob_range_last_partial_block.bin"
407 ),
408 include_bytes!("../../../../tests/golden/disclosure/chunked_range_with_offsets.bin"),
409 ];
410 for golden in goldens {
411 let (_, commit, steps, payload) = super::super::decode_disclosure(golden).unwrap();
412 let steps = metadata(&steps);
413 let predicted = match payload {
414 super::super::PayloadWire::Object { bytes } => {
415 object_proof_size(commit.len() as u64, &steps, bytes.len() as u64).unwrap()
416 }
417 super::super::PayloadWire::Range {
418 chunk: None,
419 offset_in_blob,
420 len,
421 slice,
422 ..
423 } => blob_range_proof_size(
424 commit.len() as u64,
425 &steps,
426 canonical_slice_length(&slice),
427 offset_in_blob,
428 len,
429 )
430 .unwrap(),
431 super::super::PayloadWire::Range {
432 chunk: Some(chunk),
433 offset_in_blob,
434 len,
435 slice,
436 chunk_len_proofs,
437 } => {
438 let mut lengths: Vec<u64> = chunk_len_proofs
439 .iter()
440 .map(|proof| canonical_slice_length(&proof.slice) - 10)
441 .collect();
442 let offset = lengths.iter().sum::<u64>() + offset_in_blob;
443 lengths.push(canonical_slice_length(&slice) - 10);
444 chunked_range_proof_size(
445 commit.len() as u64,
446 &steps,
447 chunk.proof.leaf_count - 1,
448 &lengths,
449 offset,
450 len,
451 )
452 .unwrap()
453 .encoded_size
454 }
455 _ => panic!("fixture selector"),
456 };
457 assert_eq!(predicted, golden.len() as u64);
458 }
459 }
460
461 #[test]
462 fn huge_metadata_is_rejected_without_proof_allocation() {
463 assert!(matches!(
464 object_proof_size(MAX_COMMIT_BYTES as u64 + 1, &[], 1),
465 Err(RangeProofError::ProofTooLarge)
466 ));
467 assert!(matches!(
468 object_proof_size(200, &[], u64::MAX),
469 Err(RangeProofError::ProofTooLarge)
470 ));
471 assert!(matches!(
472 blob_range_proof_size(200, &[], u64::MAX, 0, u64::MAX),
473 Err(RangeProofError::OffsetOverflow)
474 ));
475 assert!(matches!(
476 chunked_range_proof_size(200, &[], 1, &[1], u64::MAX, 1),
477 Err(RangeProofError::OffsetOverflow)
478 ));
479 assert!(chunked_range_proof_size(200, &[], 1, &[MAX_BUNDLE_BYTES as u64], 0, 1).is_ok());
480 assert!(matches!(
481 chunked_range_proof_size(
482 200,
483 &[],
484 2,
485 &[MAX_BUNDLE_BYTES as u64, 1],
486 0,
487 MAX_BUNDLE_BYTES as u64 + 1
488 ),
489 Err(RangeProofError::ProofTooLarge)
490 ));
491 }
492
493 #[test]
494 fn preceding_cap_and_prefix_only_metadata_are_enforced() {
495 let lengths = vec![65536; 40000];
496 assert!(matches!(
497 chunked_range_proof_size(200, &[], 50000, &lengths, 39999 * 65536, 1),
498 Err(RangeProofError::ProofTooLarge)
499 ));
500 let result = chunked_range_proof_size(200, &[], 50000, &[100], 99, 1).unwrap();
501 assert_eq!((result.first, result.last), (0, 0));
502 assert!(matches!(result.kind, RangeProofKind::Mkdp));
503 }
504
505 #[test]
506 #[allow(clippy::too_many_lines)] fn incremental_sizing_matches_builders_across_varint_and_merkle_edges() {
508 use super::super::span::{RangeProof, build_range_proof_from, verify_disclosure_span};
509 use crate::layout::RepoLayout;
510 use crate::object::{Blob, Commit, EntryMode, Identity, Object, Tree, TreeEntry};
511 use crate::serialize::serialize;
512 use crate::sign::{KeyPair, sign_commit};
513 use crate::store::ObjectStore;
514
515 let temp = tempfile::TempDir::new().unwrap();
516 let store = ObjectStore::init(&RepoLayout::single(temp.path())).unwrap();
517 for (count, name_len) in [
518 (2, 127),
519 (3, 128),
520 (7, 255),
521 (8, 1),
522 (9, 1),
523 (127, 1),
524 (128, 1),
525 (129, 1),
526 ] {
527 let lengths: Vec<u64> = (0..count)
528 .map(|i| [1, 1013, 1014, 1015, 2038][i % 5])
529 .collect();
530 let chunks: Vec<_> = lengths
531 .iter()
532 .enumerate()
533 .map(|(i, &len)| {
534 store
535 .write(
536 &serialize(&Object::Blob(Blob {
537 data: vec![i as u8; len as usize],
538 }))
539 .unwrap(),
540 )
541 .unwrap()
542 })
543 .collect();
544 let manifest = Object::ChunkedBlob(ChunkedBlob {
545 total_size: lengths.iter().sum(),
546 chunk_size: 0,
547 chunks,
548 });
549 let leaf = store.write(&serialize(&manifest).unwrap()).unwrap();
550 let name = vec![b'f'; name_len];
551 let tree = Object::Tree(Tree {
552 entries: vec![
553 TreeEntry {
554 name: b"a".to_vec(),
555 mode: EntryMode::Blob,
556 object_hash: leaf,
557 },
558 TreeEntry {
559 name: name.clone(),
560 mode: EntryMode::Blob,
561 object_hash: leaf,
562 },
563 TreeEntry {
564 name: b"z".to_vec(),
565 mode: EntryMode::Blob,
566 object_hash: leaf,
567 },
568 ],
569 });
570 let root = store.write(&serialize(&tree).unwrap()).unwrap();
571 let key = KeyPair::from_seed([3; 32]);
572 let mut commit = Commit::new_unannotated(
573 root,
574 vec![],
575 Identity::ed25519(key.public.0),
576 key.public.0,
577 vec![],
578 1,
579 [0; 64],
580 );
581 commit.signature = sign_commit(&commit, &key).unwrap().0;
582 let canonical = serialize(&Object::Commit(commit)).unwrap();
583 let id = store.write(&canonical).unwrap();
584 let steps = [PrefixStep {
585 name_len,
586 position: 1,
587 leaf_count: 3,
588 }];
589 let total = lengths.iter().sum::<u64>();
590 for (offset, len) in [(0, total), (total - 1, 1), (1, total - 2)] {
591 let plan = chunked_range_proof_size(
592 canonical.len() as u64,
593 &steps,
594 count as u32,
595 &lengths,
596 offset,
597 len,
598 )
599 .unwrap();
600 let built =
601 build_range_proof_from(&store, &id, &[&name], offset, len, None).unwrap();
602 let bytes = match built {
603 RangeProof::Mkdp(bytes) => {
604 assert_eq!(plan.kind, RangeProofKind::Mkdp);
605 super::super::verify_disclosure(&id, &bytes).unwrap();
606 bytes
607 }
608 RangeProof::Mkds(bytes) => {
609 assert_eq!(plan.kind, RangeProofKind::Mkds);
610 verify_disclosure_span(&id, &bytes).unwrap();
611 bytes
612 }
613 };
614 assert_eq!(
615 plan.encoded_size,
616 bytes.len() as u64,
617 "count={count}, offset={offset}, len={len}"
618 );
619 let mut sizer = ChunkedRangeSizer::new(
620 canonical.len() as u64,
621 &steps,
622 count as u32,
623 offset,
624 len,
625 plan.encoded_size - 1,
626 )
627 .unwrap();
628 assert!(lengths.iter().any(|&length| matches!(
629 sizer.push(length),
630 Err(RangeProofError::ProofTooLarge)
631 )));
632 }
633 }
634 }
635}