use crate::Address;
use crate::CheckpointCommitment;
use crate::CheckpointSummary;
use crate::Digest;
use crate::ObjectReference;
use crate::hash::Hasher;
use crate::merkle::MerkleError;
use crate::merkle::MerkleNonInclusionProof;
use crate::merkle::MerkleProof;
use crate::merkle::Node;
#[derive(Debug, PartialEq, Eq)]
pub enum ProofError {
InvalidMerkleProof,
MissingArtifactsDigest,
ArtifactsDigestMismatch,
}
impl std::fmt::Display for ProofError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidMerkleProof => f.write_str("invalid merkle proof"),
Self::MissingArtifactsDigest => f.write_str(
"checkpoint summary has no `CheckpointArtifacts` commitment to anchor the proof against",
),
Self::ArtifactsDigestMismatch => f.write_str(
"the checkpoint's `CheckpointArtifacts` digest does not match the proof's `tree_root`",
),
}
}
}
impl std::error::Error for ProofError {}
impl From<MerkleError> for ProofError {
fn from(_: MerkleError) -> Self {
Self::InvalidMerkleProof
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OcsInclusionProof {
pub merkle_proof: MerkleProof,
pub leaf_index: u64,
pub tree_root: Digest,
}
impl OcsInclusionProof {
pub fn verify(
&self,
summary: &CheckpointSummary,
object_ref: &ObjectReference,
) -> Result<(), ProofError> {
let tree_root_node = Node::Digest(*self.tree_root.inner());
self.merkle_proof
.verify_proof(&tree_root_node, object_ref, self.leaf_index as usize)?;
check_summary_commits_to_tree_root(summary, &self.tree_root)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OcsNonInclusionProof {
pub non_inclusion_proof: MerkleNonInclusionProof<ObjectReference>,
pub tree_root: Digest,
}
impl OcsNonInclusionProof {
pub fn verify(
&self,
summary: &CheckpointSummary,
object_id: &Address,
) -> Result<(), ProofError> {
let tree_root_node = Node::Digest(*self.tree_root.inner());
self.non_inclusion_proof.verify_proof_by_key(
&tree_root_node,
object_id,
ObjectReference::object_id,
)?;
check_summary_commits_to_tree_root(summary, &self.tree_root)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum OcsProof {
Inclusion(OcsInclusionProof),
NonInclusion(OcsNonInclusionProof),
}
fn check_summary_commits_to_tree_root(
summary: &CheckpointSummary,
tree_root: &Digest,
) -> Result<(), ProofError> {
let artifacts_digest = summary
.checkpoint_commitments
.iter()
.find_map(|c| match c {
CheckpointCommitment::CheckpointArtifacts { digest } => Some(digest),
_ => None,
})
.ok_or(ProofError::MissingArtifactsDigest)?;
let expected = compute_checkpoint_artifacts_digest(std::slice::from_ref(tree_root));
if &expected != artifacts_digest {
return Err(ProofError::ArtifactsDigestMismatch);
}
Ok(())
}
fn compute_checkpoint_artifacts_digest(artifact_digests: &[Digest]) -> Digest {
let bytes =
bcs::to_bytes(artifact_digests).expect("BCS of `&[Digest]` cannot fail for in-memory data");
Hasher::digest(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Address;
use crate::CheckpointSummary;
use crate::GasCostSummary;
use crate::merkle::MerkleTree;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
fn summary_committing_to(artifacts_digest: Digest) -> CheckpointSummary {
CheckpointSummary {
epoch: 0,
sequence_number: 0,
network_total_transactions: 0,
content_digest: Digest::ZERO,
previous_digest: None,
epoch_rolling_gas_cost_summary: GasCostSummary::default(),
timestamp_ms: 0,
checkpoint_commitments: vec![CheckpointCommitment::CheckpointArtifacts {
digest: artifacts_digest,
}],
end_of_epoch_data: None,
version_specific_data: vec![],
}
}
fn synthetic_refs(count: u8) -> Vec<ObjectReference> {
(0..count)
.map(|i| {
let mut addr = [0u8; 32];
addr[31] = i;
let mut digest = [0u8; 32];
digest[0] = i ^ 0x42;
ObjectReference::new(Address::new(addr), u64::from(i) + 1, Digest::new(digest))
})
.collect()
}
#[test]
fn inclusion_proof_verifies_against_consistent_summary() {
let refs = synthetic_refs(5);
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
let summary = summary_committing_to(artifacts_digest);
for (index, object_ref) in refs.iter().enumerate() {
let inclusion = OcsInclusionProof {
merkle_proof: tree.get_proof(index).unwrap(),
leaf_index: index as u64,
tree_root,
};
inclusion.verify(&summary, object_ref).unwrap();
}
}
#[test]
fn inclusion_proof_rejects_wrong_leaf() {
let refs = synthetic_refs(5);
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
let summary = summary_committing_to(artifacts_digest);
let inclusion = OcsInclusionProof {
merkle_proof: tree.get_proof(0).unwrap(),
leaf_index: 0,
tree_root,
};
assert_eq!(
inclusion.verify(&summary, &refs[1]),
Err(ProofError::InvalidMerkleProof),
);
}
#[test]
fn inclusion_proof_rejects_summary_with_wrong_artifacts_digest() {
let refs = synthetic_refs(3);
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let bogus_artifacts_digest = Digest::new([0xff; 32]);
let summary = summary_committing_to(bogus_artifacts_digest);
let inclusion = OcsInclusionProof {
merkle_proof: tree.get_proof(0).unwrap(),
leaf_index: 0,
tree_root,
};
assert_eq!(
inclusion.verify(&summary, &refs[0]),
Err(ProofError::ArtifactsDigestMismatch),
);
}
#[test]
fn inclusion_proof_rejects_summary_without_artifacts_commitment() {
let refs = synthetic_refs(3);
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let summary = CheckpointSummary {
epoch: 0,
sequence_number: 0,
network_total_transactions: 0,
content_digest: Digest::ZERO,
previous_digest: None,
epoch_rolling_gas_cost_summary: GasCostSummary::default(),
timestamp_ms: 0,
checkpoint_commitments: vec![],
end_of_epoch_data: None,
version_specific_data: vec![],
};
let inclusion = OcsInclusionProof {
merkle_proof: tree.get_proof(0).unwrap(),
leaf_index: 0,
tree_root,
};
assert_eq!(
inclusion.verify(&summary, &refs[0]),
Err(ProofError::MissingArtifactsDigest),
);
}
fn id(byte: u8) -> Address {
let mut addr = [0u8; 32];
addr[31] = byte;
Address::new(addr)
}
#[test]
fn non_inclusion_proof_verifies_against_consistent_summary() {
let refs = synthetic_refs(5);
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
let summary = summary_committing_to(artifacts_digest);
let missing_id = {
let mut addr = [0u8; 32];
addr[31] = 0x80;
Address::new(addr)
};
let probe = ObjectReference::new(missing_id, 0, Digest::new([0u8; 32]));
assert!(refs.iter().all(|r| r.object_id() != &missing_id));
let non_inclusion_proof = tree.compute_non_inclusion_proof(&refs, &probe).unwrap();
let proof = OcsNonInclusionProof {
non_inclusion_proof,
tree_root,
};
proof.verify(&summary, &missing_id).unwrap();
}
#[test]
fn non_inclusion_proof_rejects_present_id() {
let refs = synthetic_refs(5);
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
let summary = summary_committing_to(artifacts_digest);
let neighbour_probe = ObjectReference::new(id(0x80), 0, Digest::new([0u8; 32]));
let non_inclusion_proof = tree
.compute_non_inclusion_proof(&refs, &neighbour_probe)
.unwrap();
let proof = OcsNonInclusionProof {
non_inclusion_proof,
tree_root,
};
assert_eq!(
proof.verify(&summary, refs[1].object_id()),
Err(ProofError::InvalidMerkleProof),
);
}
#[test]
fn non_inclusion_rejects_id_strict_bracketing_violation() {
let target_id = id(0x42);
let mut refs = vec![
ObjectReference::new(id(0x00), 1, Digest::new([0x11; 32])),
ObjectReference::new(target_id, 5, Digest::new([0x22; 32])),
ObjectReference::new(id(0x80), 9, Digest::new([0x33; 32])),
];
refs.sort();
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
let summary = summary_committing_to(artifacts_digest);
let synthetic_lower = ObjectReference::new(target_id, 0, Digest::new([0u8; 32]));
let non_inclusion_proof = tree
.compute_non_inclusion_proof(&refs, &synthetic_lower)
.unwrap();
let proof = OcsNonInclusionProof {
non_inclusion_proof,
tree_root,
};
assert_eq!(
proof.verify(&summary, &target_id),
Err(ProofError::InvalidMerkleProof),
);
}
#[test]
fn checkpoint_artifacts_digest_single_artifact_shape() {
let tree_root = Digest::new([0u8; 32]);
let mut expected_input = vec![0x01u8, 0x20u8];
expected_input.extend_from_slice(tree_root.inner());
let expected = Hasher::digest(&expected_input);
let actual = compute_checkpoint_artifacts_digest(&[tree_root]);
assert_eq!(actual, expected);
}
#[test]
fn checkpoint_artifacts_digest_matches_upstream_for_zero_input() {
let actual = compute_checkpoint_artifacts_digest(&[Digest::ZERO]);
let expected = Digest::from_base58("Hu1Kq6yF9jGgTd5o9Tav3saEFSzTg7ZKehYqa8QvQXGE").unwrap();
assert_eq!(actual, expected);
}
#[cfg(feature = "proptest")]
mod proptests {
use super::*;
use proptest::collection::vec;
use proptest::prelude::*;
use test_strategy::proptest;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
fn synthetic_ref(seed: u32) -> ObjectReference {
let mut addr = [0u8; 32];
addr[28..32].copy_from_slice(&seed.to_be_bytes());
let mut digest = [0u8; 32];
digest[..4].copy_from_slice(&seed.to_le_bytes());
ObjectReference::new(
Address::new(addr),
u64::from(seed).max(1),
Digest::new(digest),
)
}
fn sorted_unique_refs() -> impl Strategy<Value = Vec<ObjectReference>> {
vec(any::<u32>(), 1..=32).prop_map(|mut seeds| {
seeds.sort();
seeds.dedup();
seeds.into_iter().map(synthetic_ref).collect()
})
}
#[proptest]
fn ocs_inclusion_proof_round_trips(
#[strategy(sorted_unique_refs())] refs: Vec<ObjectReference>,
) {
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
let summary = summary_committing_to(artifacts_digest);
for (index, object_ref) in refs.iter().enumerate() {
let proof = OcsInclusionProof {
merkle_proof: tree.get_proof(index).unwrap(),
leaf_index: index as u64,
tree_root,
};
proof.verify(&summary, object_ref).unwrap();
}
}
#[proptest]
fn ocs_inclusion_proof_rejects_summary_with_wrong_artifacts_digest(
#[strategy(sorted_unique_refs())] refs: Vec<ObjectReference>,
#[strategy(any::<[u8; 32]>())] bogus_artifacts: [u8; 32],
) {
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let correct_artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
prop_assume!(bogus_artifacts != correct_artifacts_digest.into_inner());
let summary = summary_committing_to(Digest::new(bogus_artifacts));
let proof = OcsInclusionProof {
merkle_proof: tree.get_proof(0).unwrap(),
leaf_index: 0,
tree_root,
};
prop_assert_eq!(
proof.verify(&summary, &refs[0]),
Err(ProofError::ArtifactsDigestMismatch),
);
}
#[proptest]
fn ocs_non_inclusion_proof_round_trips(
#[strategy(sorted_unique_refs())] refs: Vec<ObjectReference>,
#[strategy(any::<u32>())] target_seed: u32,
) {
prop_assume!(
refs.iter()
.all(|r| r.object_id().inner()[28..32] != target_seed.to_be_bytes())
);
let tree = MerkleTree::build_from_unserialized(&refs).unwrap();
let tree_root = Digest::new(tree.root().bytes());
let artifacts_digest = compute_checkpoint_artifacts_digest(&[tree_root]);
let summary = summary_committing_to(artifacts_digest);
let target = synthetic_ref(target_seed);
let non_inclusion = tree.compute_non_inclusion_proof(&refs, &target).unwrap();
let proof = OcsNonInclusionProof {
non_inclusion_proof: non_inclusion,
tree_root,
};
proof.verify(&summary, target.object_id()).unwrap();
}
}
}