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

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

use crate::error::CompositionError;

/// Anchor type discriminator (CDDL `anchor-type`).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AnchorType {
    /// Vauban L3 block-hash inclusion proof.
    #[serde(rename = "starknet-l3")]
    StarknetL3,
    /// Ethereum mainnet keccak-256 transaction hash.
    #[serde(rename = "ethereum-l1")]
    EthereumL1,
    /// Bitcoin OP_RETURN commitment (sha256d txid).
    #[serde(rename = "bitcoin-opreturn")]
    BitcoinOpReturn,
    /// X.509 Document Signing Certificate (RFC 5280 SubjectKeyIdentifier).
    #[serde(rename = "x509-dsc")]
    X509Dsc,
    /// DNSSEC RRSIG-protected record.
    #[serde(rename = "dnssec")]
    Dnssec,
    /// W3C DID method resolution result.
    #[serde(rename = "did-method")]
    DidMethod,
}

/// Single anchor entry (CDDL `anchor-entry`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchorEntry {
    /// Discriminator.
    #[serde(rename = "type")]
    pub anchor_type: AnchorType,
    /// Type-specific reference bytes (txid, block-hash, DER, …).
    #[serde(with = "serde_bytes")]
    pub r#ref: Vec<u8>,
    /// Optional epoch / block height.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub epoch: Option<u64>,
    /// Optional Poseidon-felt252 nullifier (revocation marker).
    #[serde(default, skip_serializing_if = "Option::is_none", with = "opt_bytes")]
    pub nullifier: Option<Vec<u8>>,
    /// Opaque per-anchor metadata.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub meta: Option<BTreeMap<String, serde_json::Value>>,
}

mod opt_bytes {
    use alloc::vec::Vec;
    use serde::{Deserialize, Deserializer, Serialize, Serializer};
    pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
        match v {
            Some(b) => serde_bytes::Bytes::new(b).serialize(s),
            None => s.serialize_none(),
        }
    }
    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
        let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(d)?;
        Ok(opt.map(serde_bytes::ByteBuf::into_vec))
    }
}

impl AnchorEntry {
    /// Validate type-specific size hints (informative per CDDL).
    pub fn validate_shape(&self) -> Result<(), CompositionError> {
        if let Some(n) = &self.nullifier {
            if n.len() != 32 {
                return Err(CompositionError::Invariant(
                    "anchor-entry.nullifier must be 32 bytes (Poseidon-felt252)",
                ));
            }
        }
        match self.anchor_type {
            AnchorType::StarknetL3 | AnchorType::EthereumL1 | AnchorType::BitcoinOpReturn => {
                if self.r#ref.len() != 32 {
                    return Err(CompositionError::Invariant(
                        "anchor-entry.ref must be 32 bytes for chain anchors",
                    ));
                }
            }
            _ => {
                if self.r#ref.is_empty() {
                    return Err(CompositionError::Invariant(
                        "anchor-entry.ref must be non-empty",
                    ));
                }
            }
        }
        Ok(())
    }
}

/// Multi-root anchor list (CDDL `anchor = [+ anchor-entry]`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Anchor(pub Vec<AnchorEntry>);

impl Anchor {
    /// Construct + enforce non-empty (I-4 / occurrence `+`).
    pub fn new(entries: Vec<AnchorEntry>) -> Result<Self, CompositionError> {
        if entries.is_empty() {
            return Err(CompositionError::Invariant(
                "anchor list must contain at least one entry (I-4)",
            ));
        }
        for e in &entries {
            e.validate_shape()?;
        }
        Ok(Self(entries))
    }

    /// Anchor URI (first entry type as string).
    pub fn uri(&self) -> &str {
        match self.0.first() {
            Some(e) => match e.anchor_type {
                AnchorType::StarknetL3 => "starknet-l3",
                AnchorType::EthereumL1 => "ethereum-l1",
                AnchorType::BitcoinOpReturn => "bitcoin-opreturn",
                AnchorType::X509Dsc => "x509-dsc",
                AnchorType::Dnssec => "dnssec",
                AnchorType::DidMethod => "did-method",
            },
            None => "unknown",
        }
    }
    /// Anchor hash (first entry ref bytes).
    pub fn hash(&self) -> &[u8] { self.0.first().map_or(&[], |e| e.r#ref.as_slice()) }
    /// Anchor epoch (first entry epoch, default 0).
    pub fn epoch(&self) -> u64 { self.0.first().and_then(|e| e.epoch).unwrap_or(0) }

    /// Set-union of two anchor lists, used by ∧ and ⊕ (C-2, G-3).
    /// Order: `self` then unique entries of `other` in original order.
    pub fn union(&self, other: &Self) -> Self {
        let mut out = self.0.clone();
        for e in &other.0 {
            if !out.iter().any(|x| x == e) {
                out.push(e.clone());
            }
        }
        Self(out)
    }
}