Skip to main content

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}