sui_sdk_types/proof.rs
1//! OCS (Object Checkpoint State) proof verification.
2//!
3//! The Object Checkpoint State is the Blake2b256 Merkle tree built by each
4//! checkpoint over the set of object references it modified, with leaves
5//! arranged in ascending `ObjectID` order (see
6//! [`crate::merkle`] for the underlying tree primitive). The tree's root
7//! is committed to by the containing [`CheckpointSummary`] via the
8//! [`CheckpointCommitment::CheckpointArtifacts`] variant of its
9//! `checkpoint_commitments`.
10//!
11//! This module defines the proof envelopes that an SDK consumer verifies
12//! against a *trusted* checkpoint summary:
13//!
14//! - [`OcsInclusionProof`] proves that a specific [`ObjectReference`]
15//! appears in the tree.
16//! - [`OcsNonInclusionProof`] proves that no leaf with a given object
17//! id appears in the tree (the OCS is keyed by object id, so this is
18//! the natural notion of "the checkpoint did not modify this object").
19//! - [`OcsProof`] tags one of the two.
20//!
21//! Verification only checks the data-relation half of the proof: it
22//! reconstructs the `CheckpointArtifactsDigest` from the proof's `tree_root`
23//! and asserts it matches the digest committed to by the summary's
24//! `CheckpointArtifacts` commitment. Authenticating the checkpoint summary
25//! itself (verifying its BLS aggregate signature against the epoch's
26//! validator committee) is a separate step performed by the caller, e.g.
27//! via `sui-crypto`'s `ValidatorCommitteeSignatureVerifier`.
28
29use crate::Address;
30use crate::CheckpointCommitment;
31use crate::CheckpointSummary;
32use crate::Digest;
33use crate::ObjectReference;
34use crate::hash::Hasher;
35use crate::merkle::MerkleError;
36use crate::merkle::MerkleNonInclusionProof;
37use crate::merkle::MerkleProof;
38use crate::merkle::Node;
39
40/// An error returned by OCS proof verification.
41#[derive(Debug, PartialEq, Eq)]
42pub enum ProofError {
43 /// The Merkle proof did not authenticate the leaf at the given index
44 /// against the proof's claimed `tree_root`.
45 InvalidMerkleProof,
46 /// The checkpoint summary's `checkpoint_commitments` did not contain a
47 /// `CheckpointArtifacts` entry — the summary cannot be used to anchor
48 /// an OCS proof.
49 MissingArtifactsDigest,
50 /// The reconstructed `CheckpointArtifactsDigest` (computed from the
51 /// proof's `tree_root`) did not match the digest committed to by the
52 /// summary's `CheckpointArtifacts` commitment.
53 ArtifactsDigestMismatch,
54}
55
56impl std::fmt::Display for ProofError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 Self::InvalidMerkleProof => f.write_str("invalid merkle proof"),
60 Self::MissingArtifactsDigest => f.write_str(
61 "checkpoint summary has no `CheckpointArtifacts` commitment to anchor the proof against",
62 ),
63 Self::ArtifactsDigestMismatch => f.write_str(
64 "the checkpoint's `CheckpointArtifacts` digest does not match the proof's `tree_root`",
65 ),
66 }
67 }
68}
69
70impl std::error::Error for ProofError {}
71
72impl From<MerkleError> for ProofError {
73 fn from(_: MerkleError) -> Self {
74 Self::InvalidMerkleProof
75 }
76}
77
78/// An OCS inclusion proof.
79///
80/// Proves that a specific [`ObjectReference`] appears at `leaf_index` in
81/// the modified-objects Merkle tree whose root is `tree_root`, and that
82/// `tree_root` is committed to by a [`CheckpointSummary`]'s
83/// `CheckpointArtifacts` commitment.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct OcsInclusionProof {
86 /// The Merkle inclusion proof for the leaf.
87 pub merkle_proof: MerkleProof,
88 /// The position of the leaf in the modified-objects tree.
89 pub leaf_index: u64,
90 /// The 32-byte Merkle root of the modified-objects tree.
91 pub tree_root: Digest,
92}
93
94impl OcsInclusionProof {
95 /// Verify that `object_ref` was written in the checkpoint described by
96 /// `summary`.
97 ///
98 /// The caller is responsible for ensuring that `summary` itself is
99 /// trusted (i.e. its BLS aggregate signature has been verified against
100 /// the epoch's validator committee). This method only checks that the
101 /// proof and the summary are consistent.
102 pub fn verify(
103 &self,
104 summary: &CheckpointSummary,
105 object_ref: &ObjectReference,
106 ) -> Result<(), ProofError> {
107 let tree_root_node = Node::Digest(*self.tree_root.inner());
108 self.merkle_proof
109 .verify_proof(&tree_root_node, object_ref, self.leaf_index as usize)?;
110 check_summary_commits_to_tree_root(summary, &self.tree_root)?;
111 Ok(())
112 }
113}
114
115/// An OCS non-inclusion proof.
116///
117/// Proves that no leaf with a given object id appears in the
118/// modified-objects Merkle tree whose root is `tree_root` — for a tree
119/// built over `ObjectReference`s in sorted order — and that `tree_root`
120/// is committed to by a [`CheckpointSummary`]'s `CheckpointArtifacts`
121/// commitment.
122///
123/// Object-id non-inclusion is strictly stronger than reference
124/// non-inclusion: the OCS keys leaves by `(object_id, version, digest)`
125/// triples, and verifying that one specific triple is absent leaves
126/// open the possibility that a different triple with the same object id
127/// is in the tree. The bracketing check enforces that the left and
128/// right neighbour leaves have object ids that strictly flank the
129/// target id, which combined with the neighbours being at adjacent
130/// indices in the sorted tree proves that no leaf under the target id
131/// can be in the tree.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct OcsNonInclusionProof {
134 /// The Merkle non-inclusion proof, holding inclusion proofs for
135 /// the bracketing neighbours of the target object id.
136 pub non_inclusion_proof: MerkleNonInclusionProof<ObjectReference>,
137 /// The 32-byte Merkle root of the modified-objects tree.
138 pub tree_root: Digest,
139}
140
141impl OcsNonInclusionProof {
142 /// Verify that no leaf with `object_id` appears in the OCS Merkle
143 /// tree committed to by `summary`.
144 ///
145 /// As with [`OcsInclusionProof::verify`], the caller is responsible
146 /// for ensuring `summary` itself is trusted.
147 ///
148 /// Stronger than verifying that a single `(object_id, version,
149 /// digest)` triple is absent: the bracketing neighbours' object ids
150 /// must strictly flank `object_id`, which combined with the
151 /// neighbours being at adjacent indices in the sorted tree proves
152 /// that no leaf with any version or digest under `object_id` is in
153 /// the tree.
154 pub fn verify(
155 &self,
156 summary: &CheckpointSummary,
157 object_id: &Address,
158 ) -> Result<(), ProofError> {
159 let tree_root_node = Node::Digest(*self.tree_root.inner());
160 self.non_inclusion_proof.verify_proof_by_key(
161 &tree_root_node,
162 object_id,
163 ObjectReference::object_id,
164 )?;
165 check_summary_commits_to_tree_root(summary, &self.tree_root)?;
166 Ok(())
167 }
168}
169
170/// An OCS proof — either an inclusion proof or a non-inclusion proof.
171///
172/// The two variants have different verification inputs: inclusion
173/// authenticates a specific [`ObjectReference`], while non-inclusion
174/// authenticates an [`Address`] (object id). Callers that need to
175/// dispatch on the variant should match on it directly.
176#[derive(Clone, Debug, PartialEq, Eq)]
177#[non_exhaustive]
178pub enum OcsProof {
179 Inclusion(OcsInclusionProof),
180 NonInclusion(OcsNonInclusionProof),
181}
182
183/// Locate the `CheckpointArtifacts` commitment on `summary`, reconstruct
184/// the expected `CheckpointArtifactsDigest` from `tree_root`, and assert
185/// equality.
186fn check_summary_commits_to_tree_root(
187 summary: &CheckpointSummary,
188 tree_root: &Digest,
189) -> Result<(), ProofError> {
190 let artifacts_digest = summary
191 .checkpoint_commitments
192 .iter()
193 .find_map(|c| match c {
194 CheckpointCommitment::CheckpointArtifacts { digest } => Some(digest),
195 _ => None,
196 })
197 .ok_or(ProofError::MissingArtifactsDigest)?;
198
199 let expected = compute_checkpoint_artifacts_digest(std::slice::from_ref(tree_root));
200
201 if &expected != artifacts_digest {
202 return Err(ProofError::ArtifactsDigestMismatch);
203 }
204 Ok(())
205}
206
207/// Reconstruct the `CheckpointArtifactsDigest` from a slice of artifact
208/// digests.
209///
210/// This mirrors upstream's
211/// `sui_types::digests::CheckpointArtifactsDigest::from_artifact_digests`,
212/// which is `BLAKE2b-256(bcs(Vec<Digest>))`. Both upstream's `Digest` and
213/// this crate's [`Digest`] BCS-encode as a one-byte length prefix (`0x20`)
214/// followed by 32 raw bytes, so a `Vec<Digest>` round-trips through BCS
215/// byte-for-byte between the two crates.
216///
217/// For the current single-artifact OCS commitment scheme `artifact_digests`
218/// is always a one-element slice containing the modified-objects
219/// `tree_root`.
220fn compute_checkpoint_artifacts_digest(artifact_digests: &[Digest]) -> Digest {
221 let bytes =
222 bcs::to_bytes(artifact_digests).expect("BCS of `&[Digest]` cannot fail for in-memory data");
223 Hasher::digest(bytes)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 use crate::Address;
231 use crate::CheckpointSummary;
232 use crate::GasCostSummary;
233 use crate::merkle::MerkleTree;
234
235 #[cfg(target_arch = "wasm32")]
236 use wasm_bindgen_test::wasm_bindgen_test as test;
237
238 /// Construct a synthetic checkpoint summary that commits to the given
239 /// `CheckpointArtifactsDigest`.
240 fn summary_committing_to(artifacts_digest: Digest) -> CheckpointSummary {
241 CheckpointSummary {
242 epoch: 0,
243 sequence_number: 0,
244 network_total_transactions: 0,
245 content_digest: Digest::ZERO,
246 previous_digest: None,
247 epoch_rolling_gas_cost_summary: GasCostSummary::default(),
248 timestamp_ms: 0,
249 checkpoint_commitments: vec![CheckpointCommitment::CheckpointArtifacts {
250 digest: artifacts_digest,
251 }],
252 end_of_epoch_data: None,
253 version_specific_data: vec![],
254 }
255 }
256
257 /// Synthesize a sorted set of object references for testing.
258 fn synthetic_refs(count: u8) -> Vec<ObjectReference> {
259 (0..count)
260 .map(|i| {
261 let mut addr = [0u8; 32];
262 addr[31] = i;
263 let mut digest = [0u8; 32];
264 digest[0] = i ^ 0x42;
265 ObjectReference::new(Address::new(addr), u64::from(i) + 1, Digest::new(digest))
266 })
267 .collect()
268 }
269
270 /// End-to-end happy path: build a tree, get a proof, wrap it in
271 /// `OcsInclusionProof`, construct a matching summary, verify.
272 #[test]
273 fn inclusion_proof_verifies_against_consistent_summary() {
274 let refs = synthetic_refs(5);
275 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
276 let tree_root = Digest::new(tree.root().bytes());
277 let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
278 let summary = summary_committing_to(artifacts_digest);
279
280 for (index, object_ref) in refs.iter().enumerate() {
281 let inclusion = OcsInclusionProof {
282 merkle_proof: tree.get_proof(index).unwrap(),
283 leaf_index: index as u64,
284 tree_root,
285 };
286 inclusion.verify(&summary, object_ref).unwrap();
287 }
288 }
289
290 /// Verifying against a leaf the proof was *not* generated for fails at
291 /// the Merkle step.
292 #[test]
293 fn inclusion_proof_rejects_wrong_leaf() {
294 let refs = synthetic_refs(5);
295 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
296 let tree_root = Digest::new(tree.root().bytes());
297 let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
298 let summary = summary_committing_to(artifacts_digest);
299
300 let inclusion = OcsInclusionProof {
301 merkle_proof: tree.get_proof(0).unwrap(),
302 leaf_index: 0,
303 tree_root,
304 };
305 assert_eq!(
306 inclusion.verify(&summary, &refs[1]),
307 Err(ProofError::InvalidMerkleProof),
308 );
309 }
310
311 /// A summary committing to the wrong `CheckpointArtifacts` digest is
312 /// rejected at the digest-comparison step.
313 #[test]
314 fn inclusion_proof_rejects_summary_with_wrong_artifacts_digest() {
315 let refs = synthetic_refs(3);
316 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
317 let tree_root = Digest::new(tree.root().bytes());
318
319 let bogus_artifacts_digest = Digest::new([0xff; 32]);
320 let summary = summary_committing_to(bogus_artifacts_digest);
321
322 let inclusion = OcsInclusionProof {
323 merkle_proof: tree.get_proof(0).unwrap(),
324 leaf_index: 0,
325 tree_root,
326 };
327 assert_eq!(
328 inclusion.verify(&summary, &refs[0]),
329 Err(ProofError::ArtifactsDigestMismatch),
330 );
331 }
332
333 /// A summary that has no `CheckpointArtifacts` commitment at all is
334 /// rejected even before the digest comparison.
335 #[test]
336 fn inclusion_proof_rejects_summary_without_artifacts_commitment() {
337 let refs = synthetic_refs(3);
338 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
339 let tree_root = Digest::new(tree.root().bytes());
340
341 let summary = CheckpointSummary {
342 epoch: 0,
343 sequence_number: 0,
344 network_total_transactions: 0,
345 content_digest: Digest::ZERO,
346 previous_digest: None,
347 epoch_rolling_gas_cost_summary: GasCostSummary::default(),
348 timestamp_ms: 0,
349 checkpoint_commitments: vec![],
350 end_of_epoch_data: None,
351 version_specific_data: vec![],
352 };
353
354 let inclusion = OcsInclusionProof {
355 merkle_proof: tree.get_proof(0).unwrap(),
356 leaf_index: 0,
357 tree_root,
358 };
359 assert_eq!(
360 inclusion.verify(&summary, &refs[0]),
361 Err(ProofError::MissingArtifactsDigest),
362 );
363 }
364
365 /// Synthesize an address whose 32nd byte is `byte`, mirroring the
366 /// id layout used by [`synthetic_refs`].
367 fn id(byte: u8) -> Address {
368 let mut addr = [0u8; 32];
369 addr[31] = byte;
370 Address::new(addr)
371 }
372
373 /// Non-inclusion happy path: ask for a proof of an id that is not
374 /// in the (sorted) tree, verify it.
375 #[test]
376 fn non_inclusion_proof_verifies_against_consistent_summary() {
377 // refs have ids 0x00..0x04; pick a target between two of them.
378 let refs = synthetic_refs(5);
379 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
380 let tree_root = Digest::new(tree.root().bytes());
381 let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
382 let summary = summary_committing_to(artifacts_digest);
383
384 // An `ObjectReference` carrying the missing id, used to drive
385 // the underlying sorted-leaf bracketing computation. The
386 // verifier only inspects the neighbours' ids, so the version
387 // and digest on this synthetic ref don't matter.
388 let missing_id = {
389 let mut addr = [0u8; 32];
390 addr[31] = 0x80;
391 Address::new(addr)
392 };
393 let probe = ObjectReference::new(missing_id, 0, Digest::new([0u8; 32]));
394 assert!(refs.iter().all(|r| r.object_id() != &missing_id));
395
396 let non_inclusion_proof = tree.compute_non_inclusion_proof(&refs, &probe).unwrap();
397 let proof = OcsNonInclusionProof {
398 non_inclusion_proof,
399 tree_root,
400 };
401 proof.verify(&summary, &missing_id).unwrap();
402 }
403
404 /// Non-inclusion fails when applied to an id that *is* in the
405 /// tree: the bracketing neighbours can't strictly flank it.
406 #[test]
407 fn non_inclusion_proof_rejects_present_id() {
408 let refs = synthetic_refs(5);
409 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
410 let tree_root = Digest::new(tree.root().bytes());
411 let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
412 let summary = summary_committing_to(artifacts_digest);
413
414 // Build a non-inclusion proof against a target adjacent (in
415 // full-reference sort order) to refs[1] but sharing no id with
416 // any leaf, then attempt to reuse the proof against refs[1]'s
417 // id itself.
418 let neighbour_probe = ObjectReference::new(id(0x80), 0, Digest::new([0u8; 32]));
419 let non_inclusion_proof = tree
420 .compute_non_inclusion_proof(&refs, &neighbour_probe)
421 .unwrap();
422 let proof = OcsNonInclusionProof {
423 non_inclusion_proof,
424 tree_root,
425 };
426 assert_eq!(
427 proof.verify(&summary, refs[1].object_id()),
428 Err(ProofError::InvalidMerkleProof),
429 );
430 }
431
432 /// A non-inclusion proof whose bracketing neighbours admit the
433 /// target's id (even though they admit some other version/digest
434 /// with the same id) is rejected. This is the strictly-stronger
435 /// guarantee that id-level non-inclusion provides over
436 /// reference-level non-inclusion: a tree containing
437 /// `(target_id, v, d)` cannot produce a non-inclusion proof for
438 /// `target_id`, even if a synthetic reference with that id and a
439 /// smaller version would sort-bracket cleanly.
440 #[test]
441 fn non_inclusion_rejects_id_strict_bracketing_violation() {
442 // Construct a tree whose three leaves include one with the
443 // target id at a specific version. Sorted by (id, version,
444 // digest), that leaf is the only leaf with the target id.
445 let target_id = id(0x42);
446 let mut refs = vec![
447 ObjectReference::new(id(0x00), 1, Digest::new([0x11; 32])),
448 ObjectReference::new(target_id, 5, Digest::new([0x22; 32])),
449 ObjectReference::new(id(0x80), 9, Digest::new([0x33; 32])),
450 ];
451 refs.sort();
452 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
453 let tree_root = Digest::new(tree.root().bytes());
454 let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
455 let summary = summary_committing_to(artifacts_digest);
456
457 // A reference that shares `target_id` but with a smaller
458 // version would sort *before* the present `(target_id, 5, _)`
459 // leaf, so the underlying merkle bracketing would succeed.
460 // The id-level check must catch this.
461 let synthetic_lower = ObjectReference::new(target_id, 0, Digest::new([0u8; 32]));
462 let non_inclusion_proof = tree
463 .compute_non_inclusion_proof(&refs, &synthetic_lower)
464 .unwrap();
465
466 let proof = OcsNonInclusionProof {
467 non_inclusion_proof,
468 tree_root,
469 };
470 assert_eq!(
471 proof.verify(&summary, &target_id),
472 Err(ProofError::InvalidMerkleProof),
473 );
474 }
475
476 /// Pin the BCS shape of `compute_checkpoint_artifacts_digest` for the
477 /// single-artifact case: `BLAKE2b-256(ULEB128(1) || 0x20 || 32 bytes)`.
478 /// The `0x20` is the length prefix on the embedded `Digest`; upstream
479 /// emits the same bytes because its `Digest` is also a 33-byte
480 /// length-prefixed BCS value.
481 #[test]
482 fn checkpoint_artifacts_digest_single_artifact_shape() {
483 let tree_root = Digest::new([0u8; 32]);
484 let mut expected_input = vec![0x01u8, 0x20u8];
485 expected_input.extend_from_slice(tree_root.inner());
486 let expected = Hasher::digest(&expected_input);
487
488 let actual = compute_checkpoint_artifacts_digest(&[tree_root]);
489 assert_eq!(actual, expected);
490 }
491
492 /// Cross-implementation pin: the artifacts digest for a one-element
493 /// `Vec<Digest>` containing the all-zero digest must match the value
494 /// produced by upstream
495 /// `sui_types::digests::CheckpointArtifactsDigest::from_artifact_digests`,
496 /// captured from a local run of upstream as base58
497 /// `Hu1Kq6yF9jGgTd5o9Tav3saEFSzTg7ZKehYqa8QvQXGE`.
498 #[test]
499 fn checkpoint_artifacts_digest_matches_upstream_for_zero_input() {
500 let actual = compute_checkpoint_artifacts_digest(&[Digest::ZERO]);
501 let expected = Digest::from_base58("Hu1Kq6yF9jGgTd5o9Tav3saEFSzTg7ZKehYqa8QvQXGE").unwrap();
502 assert_eq!(actual, expected);
503 }
504
505 /// Property-based tests for OCS proof verification. These cover the
506 /// composition of [`MerkleTree`] construction,
507 /// [`compute_checkpoint_artifacts_digest`], and the proof envelopes
508 /// over arbitrary leaf sets — complementing the synthetic spot
509 /// checks above and the real-chain fixture tests in
510 /// `tests/ocs_fixture.rs`.
511 #[cfg(feature = "proptest")]
512 mod proptests {
513 use super::*;
514
515 use proptest::collection::vec;
516 use proptest::prelude::*;
517 use test_strategy::proptest;
518
519 // See the matching comment in `merkle::tests::proptests` for why
520 // this explicit binding is needed on wasm.
521 #[cfg(target_arch = "wasm32")]
522 use wasm_bindgen_test::wasm_bindgen_test as test;
523
524 /// Derive an [`ObjectReference`] from a u32 seed. The seed lands
525 /// in the address's last 4 bytes (the high bytes are zero) so
526 /// that sorting by `ObjectReference` agrees with sorting by the
527 /// seed, which keeps the sorted-leaves invariant cheap to
528 /// reason about in the non-inclusion property below.
529 fn synthetic_ref(seed: u32) -> ObjectReference {
530 let mut addr = [0u8; 32];
531 addr[28..32].copy_from_slice(&seed.to_be_bytes());
532 let mut digest = [0u8; 32];
533 digest[..4].copy_from_slice(&seed.to_le_bytes());
534 ObjectReference::new(
535 Address::new(addr),
536 u64::from(seed).max(1),
537 Digest::new(digest),
538 )
539 }
540
541 /// Sorted, deduplicated `Vec<ObjectReference>` of length 1..=32,
542 /// generated from distinct u32 seeds. This is the shape an OCS
543 /// tree is built over.
544 fn sorted_unique_refs() -> impl Strategy<Value = Vec<ObjectReference>> {
545 vec(any::<u32>(), 1..=32).prop_map(|mut seeds| {
546 seeds.sort();
547 seeds.dedup();
548 seeds.into_iter().map(synthetic_ref).collect()
549 })
550 }
551
552 /// For any non-empty leaf set, an [`OcsInclusionProof`]
553 /// constructed by the canonical recipe (build tree, get_proof,
554 /// wrap with `tree_root`) verifies against a summary that
555 /// commits to the matching artifacts digest.
556 #[proptest]
557 fn ocs_inclusion_proof_round_trips(
558 #[strategy(sorted_unique_refs())] refs: Vec<ObjectReference>,
559 ) {
560 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
561 let tree_root = Digest::new(tree.root().bytes());
562 let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
563 let summary = summary_committing_to(artifacts_digest);
564
565 for (index, object_ref) in refs.iter().enumerate() {
566 let proof = OcsInclusionProof {
567 merkle_proof: tree.get_proof(index).unwrap(),
568 leaf_index: index as u64,
569 tree_root,
570 };
571 proof.verify(&summary, object_ref).unwrap();
572 }
573 }
574
575 /// A summary that commits to the wrong artifacts digest is
576 /// always rejected at the digest-comparison step, regardless of
577 /// the proof's merkle correctness. This pins the
578 /// [`ProofError::ArtifactsDigestMismatch`] return path for
579 /// arbitrary inputs.
580 #[proptest]
581 fn ocs_inclusion_proof_rejects_summary_with_wrong_artifacts_digest(
582 #[strategy(sorted_unique_refs())] refs: Vec<ObjectReference>,
583 #[strategy(any::<[u8; 32]>())] bogus_artifacts: [u8; 32],
584 ) {
585 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
586 let tree_root = Digest::new(tree.root().bytes());
587 let correct_artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
588 // Skip the (negligible) case where the random bogus digest
589 // happens to equal the correct one — the proof would
590 // then verify, which is the right behaviour but not the
591 // case under test.
592 prop_assume!(bogus_artifacts != correct_artifacts_digest.into_inner());
593
594 let summary = summary_committing_to(Digest::new(bogus_artifacts));
595 let proof = OcsInclusionProof {
596 merkle_proof: tree.get_proof(0).unwrap(),
597 leaf_index: 0,
598 tree_root,
599 };
600 prop_assert_eq!(
601 proof.verify(&summary, &refs[0]),
602 Err(ProofError::ArtifactsDigestMismatch),
603 );
604 }
605
606 /// For any sorted leaf set and any target that does not appear
607 /// in it, an [`OcsNonInclusionProof`] verifies against the
608 /// summary that commits to the matching artifacts digest.
609 #[proptest]
610 fn ocs_non_inclusion_proof_round_trips(
611 #[strategy(sorted_unique_refs())] refs: Vec<ObjectReference>,
612 #[strategy(any::<u32>())] target_seed: u32,
613 ) {
614 // Skip the case where the synthetic target collides with a
615 // seed already in the leaf set — the API would refuse to
616 // build a non-inclusion proof for a present leaf, and the
617 // round-trip we want to test doesn't apply.
618 prop_assume!(
619 refs.iter()
620 .all(|r| r.object_id().inner()[28..32] != target_seed.to_be_bytes())
621 );
622
623 let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
624 let tree_root = Digest::new(tree.root().bytes());
625 let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
626 let summary = summary_committing_to(artifacts_digest);
627
628 let target = synthetic_ref(target_seed);
629 let non_inclusion = tree.compute_non_inclusion_proof(&refs, &target).unwrap();
630 let proof = OcsNonInclusionProof {
631 non_inclusion_proof: non_inclusion,
632 tree_root,
633 };
634 proof.verify(&summary, target.object_id()).unwrap();
635 }
636 }
637}