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
//! Predicate primitive (CDDL §5.2).

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

use crate::error::CompositionError;

/// Predicate kind discriminator (CDDL `predicate-type`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PredicateType {
    /// Equality assertion (body = digest of asserted value).
    Equality,
    /// Range assertion (body = encoded `EncodedRange`).
    Range,
    /// Set-membership assertion (body = encoded `EncodedMembership`).
    Membership,
    /// Computation assertion (body = program-id ‖ public-output digest).
    Computation,
    /// Existence assertion (body = empty bytestring).
    Existence,
}

/// Predicate (CDDL §5.2).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Predicate {
    /// Discriminator.
    #[serde(rename = "type")]
    pub predicate_type: PredicateType,
    /// Domain URN. Examples: `urn:vauban:identity`, `iso:18013:5`.
    pub domain: String,
    /// Domain-specific opaque body. Generic processors MUST NOT introspect.
    #[serde(with = "serde_bytes")]
    pub body: Vec<u8>,
}

impl Predicate {
    /// Construct a Predicate. Existence predicates require an empty body.
    pub fn new(
        predicate_type: PredicateType,
        domain: impl Into<String>,
        body: Vec<u8>,
    ) -> Result<Self, CompositionError> {
        let p = Self {
            predicate_type,
            domain: domain.into(),
            body,
        };
        p.validate_shape()?;
        Ok(p)
    }

    /// Predicate type discriminant.
    pub fn predicate_type(&self) -> &PredicateType { &self.predicate_type }
    /// Predicate type as string (for transcript absorption).
    pub fn predicate_type_str(&self) -> &str {
        match self.predicate_type {
            PredicateType::Equality => "equality",
            PredicateType::Range => "range",
            PredicateType::Membership => "membership",
            PredicateType::Computation => "computation",
            PredicateType::Existence => "existence",
        }
    }
    /// Domain URN.
    pub fn domain(&self) -> &str { &self.domain }
    /// Opaque body bytes.
    pub fn body(&self) -> &[u8] { &self.body }

    /// Validate variant-specific body shape.
    pub fn validate_shape(&self) -> Result<(), CompositionError> {
        match (&self.predicate_type, self.body.len()) {
            (PredicateType::Existence, 0) => Ok(()),
            (PredicateType::Existence, _) => Err(CompositionError::Invariant(
                "predicate.existence requires an empty body",
            )),
            (_, 0) => Err(CompositionError::Invariant(
                "non-existence predicate requires a non-empty body",
            )),
            _ => Ok(()),
        }
    }
}

/// CDDL informative helper `encoded-range` — bounds for range predicates.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EncodedRange {
    /// Logical field name being constrained.
    pub field: String,
    /// Inclusive (or exclusive iff `open_lower`) lower bound, LE field-element bytes.
    #[serde(with = "serde_bytes")]
    pub lower: Vec<u8>,
    /// Inclusive (or exclusive iff `open_upper`) upper bound, LE field-element bytes.
    #[serde(with = "serde_bytes")]
    pub upper: Vec<u8>,
    /// Whether the lower bound is exclusive (default false).
    #[serde(default, skip_serializing_if = "core::ops::Not::not", rename = "open-lower")]
    pub open_lower: bool,
    /// Whether the upper bound is exclusive (default false).
    #[serde(default, skip_serializing_if = "core::ops::Not::not", rename = "open-upper")]
    pub open_upper: bool,
}

/// CDDL informative helper `encoded-membership` — Merkle/Verkle proof to a public set.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EncodedMembership {
    /// Public Merkle/Verkle root.
    #[serde(rename = "set-commitment", with = "serde_bytes")]
    pub set_commitment: Vec<u8>,
    /// Sibling hashes along the inclusion path.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<Vec<crate::primitives::subject::SubjectId>>,
    /// Leaf index in the tree.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub index: Option<u64>,
}