Skip to main content

proofs/
lib.rs

1//! Typed proof vocabulary shared by replicated Mere domains.
2//!
3//! P2panda operation hashes remain operation identities. These types name
4//! application commitments and content references explicitly so callers cannot
5//! silently exchange one meaning for another.
6
7use serde::{Deserialize, Serialize};
8
9/// Digest family used by a typed identity.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum DigestAlg {
12    Blake3,
13    P2pandaOperation,
14    Sha256,
15    William3,
16}
17
18/// A digest paired with its algorithm.
19#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct Digest {
21    pub alg: DigestAlg,
22    pub bytes: Vec<u8>,
23}
24
25impl Digest {
26    /// BLAKE3 digest of `bytes`.
27    pub fn blake3(bytes: &[u8]) -> Self {
28        Self {
29            alg: DigestAlg::Blake3,
30            bytes: blake3::hash(bytes).as_bytes().to_vec(),
31        }
32    }
33
34    /// Construct a typed p2panda operation identity.
35    pub fn p2panda_operation(bytes: [u8; 32]) -> Self {
36        Self {
37            alg: DigestAlg::P2pandaOperation,
38            bytes: bytes.to_vec(),
39        }
40    }
41
42    /// Read a 32-byte digest without discarding its algorithm.
43    pub fn as_32(&self) -> Result<[u8; 32], ProofTypeError> {
44        self.bytes
45            .as_slice()
46            .try_into()
47            .map_err(|_| ProofTypeError::WrongDigestLength(self.bytes.len()))
48    }
49}
50
51/// How an application commitment should be interpreted.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub enum CommitmentScheme {
54    /// Direct digest over canonical bytes, without a tree claim.
55    DigestV1,
56    MerkleV1,
57    SparseMerkleV1,
58    MmrV1,
59    AccumulatorV1,
60    VectorCommitmentV1,
61    PsiCardinalityV1,
62}
63
64/// Meaning of a commitment root.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
66pub enum CommitmentDomain {
67    MootAdmins,
68    MootMods,
69    MootStakeholders,
70    MootMembers,
71    TesseraReceipts,
72    StorageChunks,
73    StorageCheckpoints,
74    MeshJobResults,
75    DelegationSet,
76}
77
78/// Typed application-level commitment.
79#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
80pub struct Commitment {
81    pub scheme: CommitmentScheme,
82    pub domain: CommitmentDomain,
83    pub version: u16,
84    pub digest: Digest,
85}
86
87impl Commitment {
88    /// Direct version-1 commitment to canonical bytes.
89    pub fn direct(domain: CommitmentDomain, bytes: &[u8]) -> Self {
90        Self {
91            scheme: CommitmentScheme::DigestV1,
92            domain,
93            version: 1,
94            digest: Digest::blake3(bytes),
95        }
96    }
97
98    /// Whether this direct commitment matches canonical bytes and domain.
99    pub fn verifies_direct(&self, domain: CommitmentDomain, bytes: &[u8]) -> bool {
100        self.scheme == CommitmentScheme::DigestV1
101            && self.domain == domain
102            && self.version == 1
103            && self.digest == Digest::blake3(bytes)
104    }
105}
106
107/// Content-addressed blob bytes. This is deliberately not a commitment root.
108#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
109pub struct BlobRef {
110    pub digest: Digest,
111    pub byte_len: u64,
112}
113
114impl BlobRef {
115    pub fn blake3(bytes: &[u8]) -> Self {
116        Self {
117            digest: Digest::blake3(bytes),
118            byte_len: bytes.len() as u64,
119        }
120    }
121
122    pub fn verifies(&self, bytes: &[u8]) -> bool {
123        self.byte_len == bytes.len() as u64 && self.digest == Digest::blake3(bytes)
124    }
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
128pub enum ProofTypeError {
129    #[error("expected a 32-byte digest, found {0} bytes")]
130    WrongDigestLength(usize),
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn content_reference_and_application_commitment_keep_distinct_meanings() {
139        let bytes = b"checkpoint";
140        let blob = BlobRef::blake3(bytes);
141        let commitment = Commitment::direct(CommitmentDomain::StorageCheckpoints, bytes);
142        assert!(blob.verifies(bytes));
143        assert!(commitment.verifies_direct(CommitmentDomain::StorageCheckpoints, bytes));
144        assert_eq!(blob.digest, commitment.digest);
145        assert_ne!(blob.digest.alg, DigestAlg::P2pandaOperation);
146    }
147
148    #[test]
149    fn p2panda_identity_is_typed_separately() {
150        let operation = Digest::p2panda_operation([7; 32]);
151        assert_eq!(operation.alg, DigestAlg::P2pandaOperation);
152        assert_eq!(operation.as_32().unwrap(), [7; 32]);
153    }
154}