vauban-claim 0.1.0

Vauban Claim Algebra — reference implementation of draft-vauban-claim-algebra-00 (post-quantum claim sextuplet + 5 composition operators, canonical CBOR/JSON codec).
Documentation
//! Revelation Mask primitive (CDDL §5.5).

use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};

use crate::error::CompositionError;

/// Allowed digest algorithms for committed fields (CDDL `hash-alg-tag`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HashAlgTag {
    /// Default — ZK-friendly Poseidon over the Starknet base field.
    #[serde(rename = "poseidon-felt252")]
    PoseidonFelt252,
    /// Stwo native Mersenne-31 Poseidon variant.
    #[serde(rename = "poseidon-mersenne31")]
    PoseidonMersenne31,
    /// SHA-256 — non-privacy commitments only.
    #[serde(rename = "sha-256")]
    Sha256,
}

impl Default for HashAlgTag {
    fn default() -> Self {
        Self::PoseidonFelt252
    }
}

/// One committed field record (CDDL `committed-field`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommittedField {
    /// JSONPath-like dotted reference to the source field.
    pub path: String,
    /// Per-field salt (32 bytes, never reused).
    #[serde(with = "serde_bytes")]
    pub salt: Vec<u8>,
    /// `Hash(salt || canonical-encode(value))` — 32 bytes per the chosen alg.
    #[serde(with = "serde_bytes")]
    pub digest: Vec<u8>,
}

impl CommittedField {
    /// Validate salt and digest are 32 bytes (CDDL `bstr .size 32`).
    pub fn validate_shape(&self) -> Result<(), CompositionError> {
        if self.salt.len() != 32 {
            return Err(CompositionError::Invariant(
                "committed-field.salt must be 32 bytes",
            ));
        }
        if self.digest.len() != 32 {
            return Err(CompositionError::Invariant(
                "committed-field.digest must be 32 bytes",
            ));
        }
        Ok(())
    }
}

/// Revelation Mask (CDDL §5.5).
///
/// M-1 (`disclosed ∩ committed = ∅`) is enforced at construction and
/// re-checked on every operator that mutates the mask (notably ▷).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevelationMask {
    /// Field paths revealed verbatim to the Verifier.
    pub disclosed: Vec<String>,
    /// Committed fields (Poseidon-felt252 by default).
    pub committed: Vec<CommittedField>,
    /// Optional URI of a human-readable disclosure policy.
    #[serde(rename = "policy-uri", default, skip_serializing_if = "Option::is_none")]
    pub policy_uri: Option<String>,
    /// Hash algorithm used for `committed.digest`. Default Poseidon-felt252.
    #[serde(rename = "hash-alg", default, skip_serializing_if = "Option::is_none")]
    pub hash_alg: Option<HashAlgTag>,
}

impl RevelationMask {
    /// Construct and enforce M-1 + per-field shape.
    pub fn new(
        disclosed: Vec<String>,
        committed: Vec<CommittedField>,
        hash_alg: Option<HashAlgTag>,
    ) -> Result<Self, CompositionError> {
        let m = Self {
            disclosed,
            committed,
            policy_uri: None,
            hash_alg,
        };
        m.validate_shape()?;
        Ok(m)
    }

    /// Validate M-1 disjointness and CommittedField shapes.
    pub fn validate_shape(&self) -> Result<(), CompositionError> {
        for cf in &self.committed {
            cf.validate_shape()?;
        }
        let disclosed: BTreeSet<&String> = self.disclosed.iter().collect();
        for cf in &self.committed {
            if disclosed.contains(&cf.path) {
                return Err(CompositionError::MaskDisjointnessViolation);
            }
        }
        Ok(())
    }

    /// Disclosed field paths (for CDDL validation).
    pub fn disclosed(&self) -> &[String] { &self.disclosed }

    /// Committed field paths (for CDDL validation).
    pub fn committed_paths(&self) -> Vec<&str> {
        self.committed.iter().map(|cf| cf.path.as_str()).collect()
    }

    /// Whether `self` is a (non-strict) refinement of `other` (R-1 monotonicity).
    /// `self.disclosed ⊆ other.disclosed`.
    pub fn refines(&self, other: &Self) -> bool {
        let s: BTreeSet<&String> = self.disclosed.iter().collect();
        let o: BTreeSet<&String> = other.disclosed.iter().collect();
        s.is_subset(&o)
    }
}