diff --git a/CHANGELOG.md b/CHANGELOG.md index eb55d6ce..1ce1c5d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,30 @@ - The braids/strands hardening docs now define the Braid Flight Recorder and Causal X-Ray lower-mode output target over historical membership, diff, shell audit, proof-binding, and witness-posture facts. +- `warp-core` now completes the fourth braids/strands roadmap goalpost with + typed witness receipts, witness kinds, a verifier-shaped witness backend + boundary, deterministic simulator fixtures, explicit witness compatibility + rules, generic sealed membership presentations, disclosure budget labels, and + braid shell audit receipts that keep E1 self-witnessing marked as + integrity-only local evidence. The self-witness simulator rejects non-E1 + compatibility requests with a typed `UnsupportedCompatibility` error instead + of minting stable public identity for scaffolding evidence. Sealed membership + presentations validate that their witness receipt subject and evidence + digests bind the braid coordinate, purpose, authority domain, member + commitment, and disclosure budget. +- `warp-core` now completes the fifth braids/strands roadmap goalpost with a + named plurality law registry, machine-readable Law Cards, typed law + references and versions in braid shell replay/audit readings, + adapter-provided law-family routing through authority domains, and typed law + obstruction evidence for unsupported or unauthorized law execution. Law + references and braid shell policy ids reject all-zero names before replay, + collapse-derived shells report collapse policy ids as collapse laws, and law + versions reject zero before registration. Law readings derive integrity-only + posture from witness attestation strength, so non-self integrity-only receipts + are not promoted to external witness evidence, and they reject witness + receipts whose subject digest does not match the retained support digest. + Collapse-derived shells also reject records whose shell policy id diverges + from the nested collapse policy id. - `warp-core` now enforces the v1 single-writer-head strand invariant through both `Strand::new(...)` and `StrandRegistry::insert(...)`, and runtime strand forking constructs the registered relation through the same constructor diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index ec38302e..dba88b8b 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -21,11 +21,14 @@ use blake3::Hasher; use crate::admission::AdmissionOutcomeKind; use crate::ident::Hash; +use crate::plurality_law::{PluralityLawReading, PluralityLawReadingError, PluralityLawRef}; use crate::provenance_store::ProvenanceRef; use crate::revelation::{ shell_posture_obstruction, AuthorityDomainRef, CausalPosture, PostureObstruction, WitnessDigest, }; +use crate::sealed_membership::DisclosureBudget; use crate::strand::StrandId; +use crate::witness::WitnessReceipt; use crate::worldline::WorldlineId; const SHELL_DOMAIN: &[u8] = b"echo.shell.braid.v1\0"; @@ -369,9 +372,23 @@ pub enum BraidShellError { /// Collapse lineage fields must be all present or all absent. #[error("derived shell carries incoherent collapse fields")] IncoherentCollapseFields, + /// Collapse-derived shells must carry one policy identity. + #[error("derived shell collapse policy {collapse_policy:?} does not match shell policy {policy_id:?}")] + IncoherentCollapsePolicy { + /// Shell-level policy identity. + policy_id: Hash, + /// Collapse policy identity carried by the outcome. + collapse_policy: Hash, + }, /// A witness digest must never be a 32-byte shrug. #[error("empty or null witness digest refused")] EmptyWitness, + /// A policy id must name a non-empty law. + #[error("empty policy id refused")] + EmptyPolicyId, + /// A law reading failed validation. + #[error("plurality law reading refused: {0}")] + LawReading(#[from] PluralityLawReadingError), /// A lineage parent shell is missing or not plural. #[error("lineage parent {parent:?} is missing or not plural")] InvalidLineageParent { @@ -471,11 +488,13 @@ impl BraidShell { /// Returns [`BraidShellError`] for an empty member set /// ([`BraidShellError::EmptyMembers`]), a duplicate member strand or /// plural alternative ([`BraidShellError::DuplicateMemberStrand`], - /// [`BraidShellError::DuplicateAlternativeId`]), an empty witness on a + /// [`BraidShellError::DuplicateAlternativeId`]), an empty policy id + /// ([`BraidShellError::EmptyPolicyId`]), an empty witness on a /// collapse/obstruction outcome ([`BraidShellError::EmptyWitness`]), /// incoherent collapse fields - /// ([`BraidShellError::IncoherentCollapseFields`]), a posture exceeding - /// the least-revealed member + /// ([`BraidShellError::IncoherentCollapseFields`]), incoherent collapse + /// policy identity ([`BraidShellError::IncoherentCollapsePolicy`]), a + /// posture exceeding the least-revealed member /// ([`BraidShellError::PostureExceedsMembers`]), or an outcome arm that /// disagrees with member verdicts /// ([`BraidShellError::OutcomeMemberMismatch`]). @@ -520,6 +539,8 @@ impl BraidShell { if members.is_empty() { return Err(BraidShellError::EmptyMembers); } + check_policy_id(policy_id)?; + check_collapse_policy_identity(policy_id, &outcome)?; members.sort_by_cached_key(BraidShellMember::member_digest); check_unique_member_strands(&members)?; if let BraidShellOutcome::Plural { alternative_ids } = &mut outcome { @@ -600,10 +621,11 @@ impl BraidShell { /// /// Returns [`BraidShellError`] for an unsupported version, empty or /// non-canonically-ordered members, a duplicate member strand, - /// non-canonical or duplicate plural alternatives, an empty - /// collapse/obstruction witness, a posture floor violation, an - /// outcome/member disagreement, a coordinate mismatch, or a stored - /// witness/shell digest that does not match the recomputed body. + /// non-canonical or duplicate plural alternatives, an empty policy id, an + /// empty collapse/obstruction witness, incoherent collapse policy identity, + /// a posture floor violation, an outcome/member disagreement, a coordinate + /// mismatch, or a stored witness/shell digest that does not match the + /// recomputed body. pub fn validate(&self) -> Result<(), BraidShellError> { if self.version != BRAID_SHELL_VERSION { return Err(BraidShellError::UnsupportedVersion { @@ -614,6 +636,8 @@ impl BraidShell { if self.members.is_empty() { return Err(BraidShellError::EmptyMembers); } + check_policy_id(self.policy_id)?; + check_collapse_policy_identity(self.policy_id, &self.outcome)?; // Compute each member digest once; the order check, coordinate, // witness, and shell digests all consume it. let member_digests: Vec = self @@ -746,6 +770,32 @@ fn check_outcome_law(outcome: &BraidShellOutcome) -> Result<(), BraidShellError> Ok(()) } +fn check_policy_id(policy_id: Hash) -> Result<(), BraidShellError> { + if policy_id.iter().all(|byte| *byte == 0) { + return Err(BraidShellError::EmptyPolicyId); + } + Ok(()) +} + +fn check_collapse_policy_identity( + policy_id: Hash, + outcome: &BraidShellOutcome, +) -> Result<(), BraidShellError> { + if let BraidShellOutcome::Derived { + collapse_policy: Some(collapse_policy), + .. + } = outcome + { + if *collapse_policy != policy_id { + return Err(BraidShellError::IncoherentCollapsePolicy { + policy_id, + collapse_policy: *collapse_policy, + }); + } + } + Ok(()) +} + /// One strand may appear at most once among shell members. fn check_unique_member_strands(members: &[BraidShellMember]) -> Result<(), BraidShellError> { if let Some(first) = members.first() { @@ -920,8 +970,10 @@ pub struct BraidShellReplay { pub outcome_kind: AdmissionOutcomeKind, /// Member verdicts in canonical member order. pub member_verdicts: Vec<(BraidMemberRef, MemberVerdict)>, - /// Settlement policy identity the act ran under. + /// Policy identity the act ran under. pub policy_id: Hash, + /// Named law that interpreted retained plurality or collapse. + pub law_ref: PluralityLawRef, /// Witness digest binding the act. pub witness_digest: Hash, /// Revelation posture of the shell. @@ -977,6 +1029,8 @@ pub struct BraidShellMemberAuditFact { pub claim_digest: Hash, /// Digest over ordered per-claim decisions. pub verdict_digest: Hash, + /// Disclosure budget needed to interpret the member reference. + pub disclosure_budget: DisclosureBudget, } /// Replay/audit facts reproduced from a retained braid shell. @@ -988,8 +1042,10 @@ pub struct BraidShellAudit { pub coordinate: BraidCoordinate, /// Outcome arm reproduced from the shell. pub outcome_kind: AdmissionOutcomeKind, - /// Settlement policy identity the act ran under. + /// Policy identity the act ran under. pub policy_id: Hash, + /// Witnessed law reading for this shell audit. + pub law_reading: PluralityLawReading, /// Least-revealed member posture across the audited shell. pub posture_floor: CausalPosture, /// Revelation posture claimed by the shell itself. @@ -1002,6 +1058,8 @@ pub struct BraidShellAudit { pub proof_binding: BraidProofBinding, /// Witness posture for the shell. pub witness_posture: BraidWitnessPosture, + /// Typed witness receipt for the shell audit. + pub witness_receipt: WitnessReceipt, } /// Replays a braid-scope settlement outcome from retained shell records. @@ -1021,6 +1079,7 @@ pub fn replay_braid_shell( records: &dyn BraidShellRecords, ) -> Result { let shell = validated_shell_for_replay(digest, records)?; + let law_ref = shell_law_ref(shell)?; Ok(BraidShellReplay { outcome_kind: shell.outcome_kind(), member_verdicts: shell @@ -1029,6 +1088,7 @@ pub fn replay_braid_shell( .map(|member| (member.member_ref, member.verdict)) .collect(), policy_id: shell.policy_id, + law_ref, witness_digest: shell.witness_digest, posture: shell.posture, }) @@ -1066,12 +1126,21 @@ pub fn audit_braid_shell( public_inputs_hash: proof.public_inputs_hash, } }); + let witness_receipt = WitnessReceipt::self_witness(shell.digest, shell.witness_digest); + let law_ref = shell_law_ref(shell)?; + let law_reading = PluralityLawReading::new( + law_ref, + shell.digest, + witness_receipt, + shell_disclosure_budget(shell), + )?; Ok(BraidShellAudit { shell_digest: shell.digest, coordinate: shell.coordinate, outcome_kind: shell.outcome_kind(), policy_id: shell.policy_id, + law_reading, posture_floor, shell_posture: shell.posture, settlement_frontier: shell @@ -1092,15 +1161,47 @@ pub fn audit_braid_shell( footprint_digest: member.footprint_digest, claim_digest: member.claim_digest, verdict_digest: member.verdict_digest, + disclosure_budget: member_disclosure_budget(member.member_ref), }) .collect(), proof_binding, witness_posture: BraidWitnessPosture::SelfWitnessIntegrityOnly { digest: shell.witness_digest, }, + witness_receipt, }) } +const fn member_disclosure_budget(member_ref: BraidMemberRef) -> DisclosureBudget { + match member_ref { + BraidMemberRef::Revealed(_) => DisclosureBudget::Public, + BraidMemberRef::Sealed { .. } => DisclosureBudget::AuthorityScoped, + } +} + +fn shell_disclosure_budget(shell: &BraidShell) -> DisclosureBudget { + if shell + .members + .iter() + .any(|member| matches!(member.member_ref, BraidMemberRef::Sealed { .. })) + { + DisclosureBudget::AuthorityScoped + } else { + DisclosureBudget::Public + } +} + +fn shell_law_ref(shell: &BraidShell) -> Result { + match &shell.outcome { + BraidShellOutcome::Derived { + collapse_policy: Some(policy_id), + .. + } => PluralityLawRef::collapse_policy(*policy_id), + _ => PluralityLawRef::settlement_policy(shell.policy_id), + } + .map_err(|_| BraidShellError::EmptyPolicyId) +} + fn validated_shell_for_replay<'a>( digest: &Hash, records: &'a dyn BraidShellRecords, @@ -1541,6 +1642,8 @@ mod tests { #[test] fn replay_reproduces_outcome_and_member_verdicts_from_records_alone() { + use crate::plurality_law::PluralityLawRef; + let shell = plural_shell(vec![ member("member-a", MemberVerdict::Plural), member("member-b", MemberVerdict::Derived), @@ -1557,12 +1660,19 @@ mod tests { assert_eq!(replay.outcome_kind, AdmissionOutcomeKind::Plural); assert_eq!(replay.member_verdicts, expected_verdicts); assert_eq!(replay.policy_id, [0x5E; 32]); + assert_eq!( + Ok(replay.law_ref), + PluralityLawRef::settlement_policy([0x5E; 32]) + ); assert_eq!(replay.posture, CausalPosture::AuthorOnly); } #[test] fn replay_audit_reports_member_proof_support_frontier_and_witness_facts() { + use crate::plurality_law::{PluralityLawEvidencePosture, PluralityLawRef}; use crate::proof::{ProofEnvelope, ProofKind}; + use crate::sealed_membership::DisclosureBudget; + use crate::witness::{WitnessAttestation, WitnessCompatibilityRule, WitnessKind}; let members = vec![member("audit-member", MemberVerdict::Plural)]; let temp_shell = BraidShell::assemble( @@ -1603,6 +1713,19 @@ mod tests { assert_eq!(audit.shell_digest, digest); assert_eq!(audit.outcome_kind, AdmissionOutcomeKind::Plural); + assert_eq!( + Ok(audit.law_reading.law_ref()), + PluralityLawRef::settlement_policy([0x5E; 32]) + ); + assert_eq!(audit.law_reading.support_digest(), digest); + assert_eq!( + audit.law_reading.evidence_posture(), + PluralityLawEvidencePosture::SelfWitnessIntegrityOnly + ); + assert_eq!( + audit.law_reading.disclosure_budget(), + DisclosureBudget::Public + ); assert_eq!(audit.posture_floor, CausalPosture::AuthorOnly); assert_eq!(audit.shell_posture, CausalPosture::AuthorOnly); assert_eq!(audit.settlement_frontier, vec![expected_member.member_ref]); @@ -1618,6 +1741,7 @@ mod tests { footprint_digest: expected_member.footprint_digest, claim_digest: expected_member.claim_digest, verdict_digest: expected_member.verdict_digest, + disclosure_budget: DisclosureBudget::Public, }] ); assert_eq!( @@ -1634,6 +1758,47 @@ mod tests { digest: expected_witness, } ); + assert_eq!(audit.witness_receipt.kind(), WitnessKind::SelfWitness); + assert_eq!(audit.witness_receipt.subject_digest(), digest); + assert_eq!(audit.witness_receipt.evidence_digest(), expected_witness); + assert_eq!( + audit.witness_receipt.compatibility(), + WitnessCompatibilityRule::E1Scaffold + ); + assert_eq!( + audit.witness_receipt.attestation(), + WitnessAttestation::IntegrityOnly + ); + } + + #[test] + fn replay_audit_labels_sealed_member_disclosure_budget() { + use crate::sealed_membership::DisclosureBudget; + + let shell = plural_shell(vec![sealed_member( + [0xAA; 32], + authority(0x01, 0x02), + MemberVerdict::Plural, + 0x25, + )]); + let digest = shell.digest; + let records = Records::with([shell]); + + let audit = audit_braid_shell(&digest, &records).unwrap(); + + assert_eq!(audit.member_facts.len(), 1); + assert!(matches!( + audit.member_facts[0].member_ref, + BraidMemberRef::Sealed { .. } + )); + assert_eq!( + audit.member_facts[0].disclosure_budget, + DisclosureBudget::AuthorityScoped + ); + assert_eq!( + audit.law_reading.disclosure_budget(), + DisclosureBudget::AuthorityScoped + ); } #[test] @@ -1684,7 +1849,7 @@ mod tests { wl(1), basis_ref(), vec![member("member-a", MemberVerdict::Derived)], - [0x5E; 32], + [0x77; 32], BraidShellOutcome::Derived { result_refs: vec![basis_ref()], collapse_policy: Some([0x77; 32]), @@ -1713,6 +1878,22 @@ mod tests { assert_eq!(result, Err(BraidShellError::EmptyWitness)); } + #[test] + fn shell_rejects_empty_policy_id() { + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Plural)], + [0; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + ); + + assert_eq!(result, Err(BraidShellError::EmptyPolicyId)); + } + #[test] fn duplicate_alternative_ids_are_refused() { let result = BraidShell::assemble( @@ -1899,7 +2080,7 @@ mod tests { wl(1), basis_ref(), vec![member("member-a", MemberVerdict::Derived)], - [0x5E; 32], + [0x77; 32], BraidShellOutcome::Derived { result_refs: vec![basis_ref()], collapse_policy: Some([0x77; 32]), @@ -1924,6 +2105,32 @@ mod tests { ); } + #[test] + fn collapse_policy_must_match_shell_policy_id() { + let plural = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + let plural_digest = plural.digest; + + assert_eq!( + BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Derived)], + [0x5E; 32], + BraidShellOutcome::Derived { + result_refs: vec![basis_ref()], + collapse_policy: Some([0x77; 32]), + collapse_witness: Some([0x78; 32]), + collapsed_from: Some(plural_digest), + }, + CausalPosture::AuthorOnly, + ), + Err(BraidShellError::IncoherentCollapsePolicy { + policy_id: [0x5E; 32], + collapse_policy: [0x77; 32], + }) + ); + } + #[test] fn tampering_with_version_fails_validation() { let mut shell = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); @@ -1981,6 +2188,8 @@ mod tests { #[test] fn collapse_with_named_policy_derives_without_mutating_the_plural_parent() { + use crate::plurality_law::PluralityLawFamily; + let plural = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); let plural_digest = plural.digest; let snapshot = plural.clone(); @@ -2011,6 +2220,12 @@ mod tests { store.0.insert(derived.digest, derived.clone()); let replay = replay_braid_shell(&derived.digest, &store).unwrap(); assert_eq!(replay.outcome_kind, AdmissionOutcomeKind::Derived); + assert_eq!(replay.law_ref.family(), PluralityLawFamily::Collapse); + let audit = audit_braid_shell(&derived.digest, &store).unwrap(); + assert_eq!( + audit.law_reading.law_ref().family(), + PluralityLawFamily::Collapse + ); } #[test] diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 34c9154e..a89c73fb 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -139,6 +139,7 @@ mod optic_artifact; pub mod parallel; mod payload; mod playback; +mod plurality_law; pub mod proof; mod provenance_store; mod receipt; @@ -149,6 +150,7 @@ mod revelation; mod rule; mod sandbox; mod scheduler; +mod sealed_membership; mod settlement; mod snapshot; mod snapshot_accum; @@ -159,6 +161,7 @@ mod tick_patch; mod trusted_runtime_host; mod tx; mod warp_state; +mod witness; mod witnessed_suffix; #[cfg(test)] mod witnessed_suffix_tests; @@ -243,6 +246,14 @@ pub use payload::{ encode_motion_payload_q32_32, encode_motion_payload_v0, motion_payload_type_id, motion_payload_type_id_v0, }; +// --- Plurality law types --- +pub use plurality_law::{ + PluralityLawAuthorization, PluralityLawCard, PluralityLawCardError, PluralityLawConcealment, + PluralityLawEmission, PluralityLawEvidencePosture, PluralityLawFamily, PluralityLawName, + PluralityLawObstruction, PluralityLawObstructionKind, PluralityLawReading, + PluralityLawReadingError, PluralityLawRef, PluralityLawRefError, PluralityLawRegistry, + PluralityLawRegistryError, PluralityLawRequirement, +}; // --- Cursor types --- pub use contract_obstruction::{ ContractObstruction, ContractObstructionKind, ContractObstructionSubject, @@ -253,12 +264,23 @@ pub use playback::{ pub use retained_evidence::{ RetainedEvidenceCoordinate, RetainedEvidencePosture, RetainedEvidenceRef, RetainedEvidenceRole, }; +// --- Sealed membership capability types --- +pub use sealed_membership::{ + DisclosureBudget, PresentationPurpose, SealedMembershipPresentation, + SealedMembershipPresentationError, +}; // --- Session types --- pub use playback::{SessionId, ViewSession}; // --- Proof types --- pub use proof::{ ObserverHonestyClaim, ProofEnvelope, ProofError, ProofKind, VerificationFailureCode, }; +// --- Witness receipt types --- +pub use witness::{ + WitnessAttestation, WitnessBackend, WitnessBackendSimulator, WitnessCompatibilityRule, + WitnessError, WitnessKind, WitnessReceipt, WitnessRejectionCode, WitnessRequest, + WitnessSimulatorFixture, +}; // --- Braid Log types --- pub use braid::{ Braid, BraidError, BraidEvent, BraidMembershipCursor, BraidMembershipDiff, diff --git a/crates/warp-core/src/plurality_law.rs b/crates/warp-core/src/plurality_law.rs new file mode 100644 index 00000000..94f961ee --- /dev/null +++ b/crates/warp-core/src/plurality_law.rs @@ -0,0 +1,736 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Named plurality law registry, cards, readings, and obstruction evidence. + +use std::collections::BTreeMap; + +use blake3::Hasher; +use thiserror::Error; + +use crate::ident::Hash; +use crate::revelation::AuthorityDomainRef; +use crate::sealed_membership::DisclosureBudget; +use crate::witness::{WitnessAttestation, WitnessReceipt}; + +const LAW_NAME_DOMAIN: &[u8] = b"echo.plurality-law.name.v1\0"; +const LAW_REF_DOMAIN: &[u8] = b"echo.plurality-law.ref.v1\0"; +const LAW_CARD_DOMAIN: &[u8] = b"echo.plurality-law.card.v1\0"; +const LAW_READING_DOMAIN: &[u8] = b"echo.plurality-law.reading.v1\0"; +const LAW_OBSTRUCTION_DOMAIN: &[u8] = b"echo.plurality-law.obstruction.v1\0"; + +/// Hash-backed plurality law name. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PluralityLawName(Hash); + +impl PluralityLawName { + /// Constructs a law name from canonical bytes. + #[must_use] + pub const fn from_bytes(bytes: Hash) -> Self { + Self(bytes) + } + + /// Derives a law name from a stable label. + #[must_use] + pub fn from_label(label: &str) -> Self { + let mut hasher = Hasher::new(); + hasher.update(LAW_NAME_DOMAIN); + hash_len(&mut hasher, label.len()); + hasher.update(label.as_bytes()); + Self(hasher.finalize().into()) + } + + /// Returns the canonical law-name bytes. + #[must_use] + pub const fn as_bytes(&self) -> &Hash { + &self.0 + } +} + +/// Plurality law family. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PluralityLawFamily { + /// Settlement law family. + Settlement, + /// Collapse law family. + Collapse, + /// Conflict-preserving law family. + ConflictPreserving, + /// Quorum law family. + Quorum, + /// Authority law family. + Authority, + /// Adapter-provided law family scoped to an authority-domain digest. + AdapterProvided { + /// Digest of the authority domain that owns the adapter-provided law family. + authority_digest: Hash, + }, +} + +impl PluralityLawFamily { + /// Constructs an adapter-provided law family from an authority domain. + #[must_use] + pub fn adapter_provided(authority: AuthorityDomainRef) -> Self { + Self::AdapterProvided { + authority_digest: authority_digest(authority), + } + } + + fn hash_into(self, hasher: &mut Hasher) { + match self { + Self::Settlement => { + hasher.update(&[0x01]); + } + Self::Collapse => { + hasher.update(&[0x02]); + } + Self::ConflictPreserving => { + hasher.update(&[0x03]); + } + Self::Quorum => { + hasher.update(&[0x04]); + } + Self::Authority => { + hasher.update(&[0x05]); + } + Self::AdapterProvided { authority_digest } => { + hasher.update(&[0x06]); + hasher.update(&authority_digest); + } + } + } +} + +/// Error raised when constructing an invalid plurality law reference. +#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)] +pub enum PluralityLawRefError { + /// Law names must not be the all-zero digest. + #[error("plurality law name must be non-empty")] + EmptyName, + /// Law versions start at 1. + #[error("plurality law version must be non-zero")] + ZeroVersion, +} + +/// Name, family, and version for a plurality law. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PluralityLawRef { + family: PluralityLawFamily, + name: PluralityLawName, + version: u32, +} + +impl PluralityLawRef { + /// Constructs a named, versioned plurality law reference. + /// + /// # Errors + /// + /// Returns [`PluralityLawRefError::EmptyName`] when `name` is the all-zero + /// digest. + /// + /// Returns [`PluralityLawRefError::ZeroVersion`] when `version` is zero. + pub fn new( + family: PluralityLawFamily, + name: PluralityLawName, + version: u32, + ) -> Result { + if name.as_bytes().iter().all(|byte| *byte == 0) { + return Err(PluralityLawRefError::EmptyName); + } + if version == 0 { + return Err(PluralityLawRefError::ZeroVersion); + } + Ok(Self { + family, + name, + version, + }) + } + + /// Constructs the v1 settlement-law reference for an existing policy id. + /// + /// # Errors + /// + /// Returns [`PluralityLawRefError::EmptyName`] when `policy_id` is the + /// all-zero digest. + pub fn settlement_policy(policy_id: Hash) -> Result { + Self::new( + PluralityLawFamily::Settlement, + PluralityLawName::from_bytes(policy_id), + 1, + ) + } + + /// Constructs the v1 collapse-law reference for an existing policy id. + /// + /// # Errors + /// + /// Returns [`PluralityLawRefError::EmptyName`] when `policy_id` is the + /// all-zero digest. + pub fn collapse_policy(policy_id: Hash) -> Result { + Self::new( + PluralityLawFamily::Collapse, + PluralityLawName::from_bytes(policy_id), + 1, + ) + } + + /// Returns the law family. + #[must_use] + pub const fn family(self) -> PluralityLawFamily { + self.family + } + + /// Returns the law name. + #[must_use] + pub const fn name(self) -> PluralityLawName { + self.name + } + + /// Returns the law version. + #[must_use] + pub const fn version(self) -> u32 { + self.version + } + + /// Returns the canonical law reference digest. + #[must_use] + pub fn digest(self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(LAW_REF_DOMAIN); + self.hash_into(&mut hasher); + hasher.finalize().into() + } + + fn hash_into(self, hasher: &mut Hasher) { + self.family.hash_into(hasher); + hasher.update(self.name.as_bytes()); + hasher.update(&self.version.to_le_bytes()); + } +} + +/// Machine-readable requirement named by a plurality Law Card. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PluralityLawRequirement { + /// The law requires member support pins. + SupportPins, + /// The law requires the retained settlement frontier digest. + FrontierDigest, + /// The law requires the posture floor. + PostureFloor, + /// The law requires proof-envelope binding. + ProofBinding, + /// The law requires witness receipt evidence. + WitnessReceipt, + /// The law requires a capability presentation. + CapabilityPresentation, + /// The law requires an explicit disclosure budget. + DisclosureBudget, +} + +impl PluralityLawRequirement { + const fn tag(self) -> u8 { + match self { + Self::SupportPins => 0x01, + Self::FrontierDigest => 0x02, + Self::PostureFloor => 0x03, + Self::ProofBinding => 0x04, + Self::WitnessReceipt => 0x05, + Self::CapabilityPresentation => 0x06, + Self::DisclosureBudget => 0x07, + } + } +} + +/// Machine-readable emission named by a plurality Law Card. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PluralityLawEmission { + /// The law emits a plural artifact. + PluralArtifact, + /// The law emits a derived shell. + DerivedShell, + /// The law emits conflict residue. + ConflictResidue, + /// The law emits obstruction evidence. + ObstructionEvidence, + /// The law emits an audit reading. + AuditReading, +} + +impl PluralityLawEmission { + const fn tag(self) -> u8 { + match self { + Self::PluralArtifact => 0x01, + Self::DerivedShell => 0x02, + Self::ConflictResidue => 0x03, + Self::ObstructionEvidence => 0x04, + Self::AuditReading => 0x05, + } + } +} + +/// Machine-readable concealed material named by a plurality Law Card. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PluralityLawConcealment { + /// The law conceals sealed member source chains. + SealedMemberSourceChain, + /// The law conceals member blinding material. + MemberBlindingMaterial, + /// The law conceals private member history. + PrivateMemberHistory, +} + +impl PluralityLawConcealment { + const fn tag(self) -> u8 { + match self { + Self::SealedMemberSourceChain => 0x01, + Self::MemberBlindingMaterial => 0x02, + Self::PrivateMemberHistory => 0x03, + } + } +} + +/// Evidence posture attached to plurality law execution. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PluralityLawEvidencePosture { + /// Integrity-only evidence, including E1 self-witness scaffolding. + SelfWitnessIntegrityOnly, + /// Independent external witness evidence. + ExternalWitness, +} + +impl PluralityLawEvidencePosture { + const fn tag(self) -> u8 { + match self { + Self::SelfWitnessIntegrityOnly => 0x01, + Self::ExternalWitness => 0x02, + } + } +} + +/// Error raised when constructing an invalid Law Card. +#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)] +pub enum PluralityLawCardError { + /// A Law Card must name at least one emitted artifact or reading. + #[error("plurality Law Card must name at least one emission")] + MissingEmission, +} + +/// Machine-readable Law Card. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PluralityLawCard { + law_ref: PluralityLawRef, + requires: Vec, + emits: Vec, + conceals: Vec, + evidence_posture: PluralityLawEvidencePosture, +} + +impl PluralityLawCard { + /// Constructs a machine-readable Law Card. + /// + /// Requirements, emissions, and concealments are sorted and deduplicated so + /// card identity does not depend on caller vector ordering. + /// + /// # Errors + /// + /// Returns [`PluralityLawCardError::MissingEmission`] when the card names + /// no emitted artifact, obstruction, or reading. + pub fn new( + law_ref: PluralityLawRef, + requires: Vec, + emits: Vec, + conceals: Vec, + evidence_posture: PluralityLawEvidencePosture, + ) -> Result { + let emits = normalized(emits); + if emits.is_empty() { + return Err(PluralityLawCardError::MissingEmission); + } + Ok(Self { + law_ref, + requires: normalized(requires), + emits, + conceals: normalized(conceals), + evidence_posture, + }) + } + + /// Returns the law reference named by this card. + #[must_use] + pub const fn law_ref(&self) -> PluralityLawRef { + self.law_ref + } + + /// Returns the law version. + #[must_use] + pub const fn version(&self) -> u32 { + self.law_ref.version() + } + + /// Returns required evidence/support facts. + #[must_use] + pub fn requires(&self) -> &[PluralityLawRequirement] { + &self.requires + } + + /// Returns emitted artifact/reading classes. + #[must_use] + pub fn emits(&self) -> &[PluralityLawEmission] { + &self.emits + } + + /// Returns concealed material classes. + #[must_use] + pub fn conceals(&self) -> &[PluralityLawConcealment] { + &self.conceals + } + + /// Returns the evidence posture for this Law Card. + #[must_use] + pub const fn evidence_posture(&self) -> PluralityLawEvidencePosture { + self.evidence_posture + } + + /// Returns the canonical Law Card digest. + #[must_use] + pub fn digest(&self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(LAW_CARD_DOMAIN); + self.law_ref.hash_into(&mut hasher); + hash_tag_vec( + &mut hasher, + self.requires + .iter() + .copied() + .map(PluralityLawRequirement::tag), + ); + hash_tag_vec( + &mut hasher, + self.emits.iter().copied().map(PluralityLawEmission::tag), + ); + hash_tag_vec( + &mut hasher, + self.conceals + .iter() + .copied() + .map(PluralityLawConcealment::tag), + ); + hasher.update(&[self.evidence_posture.tag()]); + hasher.finalize().into() + } +} + +/// Registry errors raised while registering Law Cards. +#[derive(Error, Clone, Debug, PartialEq, Eq)] +pub enum PluralityLawRegistryError { + /// A Law Card for this reference is already registered. + #[error("plurality law is already registered: {law_ref:?}")] + DuplicateLaw { + /// Duplicate law reference. + law_ref: PluralityLawRef, + }, +} + +/// Deterministic plurality law registry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct PluralityLawRegistry { + cards: BTreeMap, +} + +impl PluralityLawRegistry { + /// Creates an empty plurality law registry. + #[must_use] + pub const fn new() -> Self { + Self { + cards: BTreeMap::new(), + } + } + + /// Registers a Law Card. + /// + /// # Errors + /// + /// Returns [`PluralityLawRegistryError::DuplicateLaw`] when a card with + /// the same law reference is already registered. + pub fn register(&mut self, card: PluralityLawCard) -> Result<(), PluralityLawRegistryError> { + let law_ref = card.law_ref(); + if self.cards.contains_key(&law_ref) { + return Err(PluralityLawRegistryError::DuplicateLaw { law_ref }); + } + self.cards.insert(law_ref, card); + Ok(()) + } + + /// Returns a registered Law Card. + #[must_use] + pub fn card(&self, law_ref: &PluralityLawRef) -> Option<&PluralityLawCard> { + self.cards.get(law_ref) + } + + /// Returns the number of registered Law Cards. + #[must_use] + pub fn len(&self) -> usize { + self.cards.len() + } + + /// Returns true when the registry contains no Law Cards. + #[must_use] + pub fn is_empty(&self) -> bool { + self.cards.is_empty() + } + + /// Authorizes execution of a registered law. + /// + /// # Errors + /// + /// Returns [`PluralityLawObstruction`] when the law is unsupported by this + /// registry or when an adapter-provided law is not authorized by its + /// owning authority domain. + pub fn authorize( + &self, + law_ref: PluralityLawRef, + authorized_by: Option, + ) -> Result { + let card = self.card(&law_ref).ok_or_else(|| { + PluralityLawObstruction::new( + PluralityLawObstructionKind::UnsupportedLaw, + law_ref, + authorized_by, + ) + })?; + if let PluralityLawFamily::AdapterProvided { + authority_digest: required_authority, + } = law_ref.family() + { + if authorized_by.map(authority_digest) != Some(required_authority) { + return Err(PluralityLawObstruction::new( + PluralityLawObstructionKind::UnauthorizedLaw, + law_ref, + authorized_by, + )); + } + } + Ok(PluralityLawAuthorization { + law_ref, + card_digest: card.digest(), + authorized_by, + }) + } +} + +/// Successful law authorization evidence. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PluralityLawAuthorization { + /// Authorized law reference. + pub law_ref: PluralityLawRef, + /// Digest of the registered Law Card. + pub card_digest: Hash, + /// Authority that authorized execution, if any. + pub authorized_by: Option, +} + +/// Typed plurality law obstruction kind. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PluralityLawObstructionKind { + /// The requested law is not registered. + UnsupportedLaw, + /// The requested law exists but lacks required authority. + UnauthorizedLaw, +} + +impl PluralityLawObstructionKind { + const fn tag(self) -> u8 { + match self { + Self::UnsupportedLaw => 0x01, + Self::UnauthorizedLaw => 0x02, + } + } +} + +/// Typed obstruction evidence for plurality law execution. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PluralityLawObstruction { + /// Obstruction kind. + pub kind: PluralityLawObstructionKind, + /// Law reference that could not execute. + pub law_ref: PluralityLawRef, + /// Authority-domain digest that attempted to authorize execution, if any. + pub authorized_by: Option, +} + +impl PluralityLawObstruction { + /// Constructs typed law obstruction evidence. + #[must_use] + pub fn new( + kind: PluralityLawObstructionKind, + law_ref: PluralityLawRef, + authorized_by: Option, + ) -> Self { + let authorized_by = authorized_by.map(authority_digest); + Self { + kind, + law_ref, + authorized_by, + } + } + + /// Returns the canonical obstruction digest. + #[must_use] + pub fn digest(self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(LAW_OBSTRUCTION_DOMAIN); + hasher.update(&[self.kind.tag()]); + self.law_ref.hash_into(&mut hasher); + hash_optional_digest(&mut hasher, self.authorized_by); + hasher.finalize().into() + } +} + +/// Witnessed plurality law reading identity. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PluralityLawReading { + law_ref: PluralityLawRef, + support_digest: Hash, + witness_receipt: WitnessReceipt, + evidence_posture: PluralityLawEvidencePosture, + disclosure_budget: DisclosureBudget, +} + +/// Error raised when constructing an invalid plurality law reading. +#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)] +pub enum PluralityLawReadingError { + /// The witness receipt names different support than the reading. + #[error( + "plurality law reading witness subject mismatch: expected {expected:?}, actual {actual:?}" + )] + WitnessSubjectMismatch { + /// Support digest claimed by the reading. + expected: Hash, + /// Subject digest named by the witness receipt. + actual: Hash, + }, +} + +impl PluralityLawReading { + /// Constructs a witnessed plurality law reading. + /// + /// # Errors + /// + /// Returns [`PluralityLawReadingError::WitnessSubjectMismatch`] when the + /// witness receipt subject does not match the retained support digest. + pub fn new( + law_ref: PluralityLawRef, + support_digest: Hash, + witness_receipt: WitnessReceipt, + disclosure_budget: DisclosureBudget, + ) -> Result { + let actual = witness_receipt.subject_digest(); + if actual != support_digest { + return Err(PluralityLawReadingError::WitnessSubjectMismatch { + expected: support_digest, + actual, + }); + } + let evidence_posture = evidence_posture_for(witness_receipt); + Ok(Self { + law_ref, + support_digest, + witness_receipt, + evidence_posture, + disclosure_budget, + }) + } + + /// Returns the law reference used for this reading. + #[must_use] + pub const fn law_ref(self) -> PluralityLawRef { + self.law_ref + } + + /// Returns the retained support digest interpreted by the law. + #[must_use] + pub const fn support_digest(self) -> Hash { + self.support_digest + } + + /// Returns the witness receipt supporting this reading. + #[must_use] + pub const fn witness_receipt(self) -> WitnessReceipt { + self.witness_receipt + } + + /// Returns the evidence posture for this reading. + #[must_use] + pub const fn evidence_posture(self) -> PluralityLawEvidencePosture { + self.evidence_posture + } + + /// Returns the disclosure budget for this reading. + #[must_use] + pub const fn disclosure_budget(self) -> DisclosureBudget { + self.disclosure_budget + } + + /// Returns the canonical law reading digest. + #[must_use] + pub fn digest(self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(LAW_READING_DOMAIN); + self.law_ref.hash_into(&mut hasher); + hasher.update(&self.support_digest); + hasher.update(&self.witness_receipt.digest()); + hasher.update(&[self.evidence_posture.tag()]); + hasher.update(&[disclosure_budget_tag(self.disclosure_budget)]); + hasher.finalize().into() + } +} + +fn evidence_posture_for(receipt: WitnessReceipt) -> PluralityLawEvidencePosture { + match receipt.attestation() { + WitnessAttestation::IntegrityOnly => PluralityLawEvidencePosture::SelfWitnessIntegrityOnly, + WitnessAttestation::IndependentAttestation => PluralityLawEvidencePosture::ExternalWitness, + } +} + +fn normalized(mut values: Vec) -> Vec { + values.sort(); + values.dedup(); + values +} + +fn hash_len(hasher: &mut Hasher, len: usize) { + hasher.update(&(len as u64).to_le_bytes()); +} + +fn hash_tag_vec(hasher: &mut Hasher, tags: impl ExactSizeIterator) { + hash_len(hasher, tags.len()); + for tag in tags { + hasher.update(&[tag]); + } +} + +fn authority_digest(authority: AuthorityDomainRef) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(authority.origin_id.as_bytes()); + hasher.update(authority.domain_id.as_bytes()); + hasher.finalize().into() +} + +fn hash_optional_digest(hasher: &mut Hasher, digest: Option) { + if let Some(digest) = digest { + hasher.update(&[0x01]); + hasher.update(&digest); + } else { + hasher.update(&[0x00]); + } +} + +const fn disclosure_budget_tag(budget: DisclosureBudget) -> u8 { + match budget { + DisclosureBudget::Public => 0x01, + DisclosureBudget::AuthorityScoped => 0x02, + DisclosureBudget::CapabilityScoped => 0x03, + DisclosureBudget::HolderOnly => 0x04, + DisclosureBudget::ZeroKnowledge => 0x05, + } +} diff --git a/crates/warp-core/src/sealed_membership.rs b/crates/warp-core/src/sealed_membership.rs new file mode 100644 index 00000000..a01fc763 --- /dev/null +++ b/crates/warp-core/src/sealed_membership.rs @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Purpose-bound sealed membership presentation vocabulary. + +use blake3::Hasher; +use thiserror::Error; + +use crate::braid_shell::BraidCoordinate; +use crate::ident::Hash; +use crate::revelation::AuthorityDomainRef; +use crate::witness::WitnessReceipt; + +const PRESENTATION_SUBJECT_DOMAIN: &[u8] = b"echo.sealed-membership.presentation.subject.v1\0"; +const PRESENTATION_EVIDENCE_DOMAIN: &[u8] = b"echo.sealed-membership.presentation.evidence.v1\0"; + +/// Disclosure budget attached to replay and sealed membership facts. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DisclosureBudget { + /// Publicly revealed material. + Public, + /// Material scoped to an authority domain. + AuthorityScoped, + /// Material scoped to a capability presentation. + CapabilityScoped, + /// Material visible only to the holder. + HolderOnly, + /// Zero-knowledge disclosure budget. + ZeroKnowledge, +} + +/// Generic capability purpose for sealed membership presentations. +/// +/// This type is deliberately generic. Application-domain purpose names belong +/// in adapters or authored contracts, not in Echo core. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PresentationPurpose { + purpose_id: Hash, +} + +impl PresentationPurpose { + /// Creates a generic presentation purpose from a caller-defined digest. + #[must_use] + pub const fn new(purpose_id: Hash) -> Self { + Self { purpose_id } + } + + /// Returns the purpose digest. + #[must_use] + pub const fn purpose_id(self) -> Hash { + self.purpose_id + } +} + +/// Error raised when a sealed membership presentation is not bound to its witness. +#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)] +pub enum SealedMembershipPresentationError { + /// The witness receipt subject does not match the presentation claim. + #[error("sealed membership witness subject mismatch")] + WitnessSubjectMismatch { + /// Expected subject digest for the presentation fields. + expected: Hash, + /// Actual subject digest carried by the witness receipt. + actual: Hash, + }, + /// The witness receipt evidence does not match the presentation claim. + #[error("sealed membership witness evidence mismatch")] + WitnessEvidenceMismatch { + /// Expected evidence digest for the presentation fields. + expected: Hash, + /// Actual evidence digest carried by the witness receipt. + actual: Hash, + }, +} + +/// Purpose-bound sealed membership presentation. +/// +/// A presentation proves only a membership claim for a braid coordinate, +/// authority domain, generic purpose, and disclosure budget. It does not reveal +/// global strand identity or source history by construction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SealedMembershipPresentation { + /// Braid coordinate the membership claim is scoped to. + braid_coordinate: BraidCoordinate, + /// Generic capability purpose for the presentation. + purpose: PresentationPurpose, + /// Authority domain under which the sealed member commitment is meaningful. + authority_domain: AuthorityDomainRef, + /// Blinded member commitment presented for the purpose. + member_commitment: Hash, + /// Witness receipt supporting this presentation. + witness_receipt: WitnessReceipt, + /// Disclosure budget governing the presentation. + disclosure_budget: DisclosureBudget, +} + +impl SealedMembershipPresentation { + /// Creates a purpose-bound sealed membership presentation. + /// + /// # Errors + /// + /// Returns [`SealedMembershipPresentationError`] when the witness receipt + /// subject or evidence digest does not match the presentation fields. + pub fn new( + braid_coordinate: BraidCoordinate, + purpose: PresentationPurpose, + authority_domain: AuthorityDomainRef, + member_commitment: Hash, + witness_receipt: WitnessReceipt, + disclosure_budget: DisclosureBudget, + ) -> Result { + let expected_subject = Self::witness_subject_digest( + braid_coordinate, + purpose, + authority_domain, + member_commitment, + disclosure_budget, + ); + if witness_receipt.subject_digest() != expected_subject { + return Err(SealedMembershipPresentationError::WitnessSubjectMismatch { + expected: expected_subject, + actual: witness_receipt.subject_digest(), + }); + } + let expected_evidence = Self::witness_evidence_digest( + braid_coordinate, + purpose, + authority_domain, + member_commitment, + disclosure_budget, + ); + if witness_receipt.evidence_digest() != expected_evidence { + return Err(SealedMembershipPresentationError::WitnessEvidenceMismatch { + expected: expected_evidence, + actual: witness_receipt.evidence_digest(), + }); + } + Ok(Self { + braid_coordinate, + purpose, + authority_domain, + member_commitment, + witness_receipt, + disclosure_budget, + }) + } + + /// Returns the witness subject digest for these presentation fields. + #[must_use] + pub fn witness_subject_digest( + braid_coordinate: BraidCoordinate, + purpose: PresentationPurpose, + authority_domain: AuthorityDomainRef, + member_commitment: Hash, + disclosure_budget: DisclosureBudget, + ) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(PRESENTATION_SUBJECT_DOMAIN); + hash_presentation_fields( + &mut hasher, + braid_coordinate, + purpose, + authority_domain, + member_commitment, + disclosure_budget, + ); + hasher.finalize().into() + } + + /// Returns the witness evidence digest for these presentation fields. + #[must_use] + pub fn witness_evidence_digest( + braid_coordinate: BraidCoordinate, + purpose: PresentationPurpose, + authority_domain: AuthorityDomainRef, + member_commitment: Hash, + disclosure_budget: DisclosureBudget, + ) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(PRESENTATION_EVIDENCE_DOMAIN); + hash_presentation_fields( + &mut hasher, + braid_coordinate, + purpose, + authority_domain, + member_commitment, + disclosure_budget, + ); + hasher.finalize().into() + } + + /// Returns the braid coordinate the membership claim is scoped to. + #[must_use] + pub const fn braid_coordinate(self) -> BraidCoordinate { + self.braid_coordinate + } + + /// Returns the generic capability purpose for the presentation. + #[must_use] + pub const fn purpose(self) -> PresentationPurpose { + self.purpose + } + + /// Returns the authority domain under which the commitment is meaningful. + #[must_use] + pub const fn authority_domain(self) -> AuthorityDomainRef { + self.authority_domain + } + + /// Returns the blinded member commitment presented for the purpose. + #[must_use] + pub const fn member_commitment(self) -> Hash { + self.member_commitment + } + + /// Returns the witness receipt supporting this presentation. + #[must_use] + pub const fn witness_receipt(self) -> WitnessReceipt { + self.witness_receipt + } + + /// Returns the disclosure budget governing the presentation. + #[must_use] + pub const fn disclosure_budget(self) -> DisclosureBudget { + self.disclosure_budget + } +} + +fn hash_presentation_fields( + hasher: &mut Hasher, + braid_coordinate: BraidCoordinate, + purpose: PresentationPurpose, + authority_domain: AuthorityDomainRef, + member_commitment: Hash, + disclosure_budget: DisclosureBudget, +) { + hasher.update(&braid_coordinate.0); + hasher.update(&purpose.purpose_id()); + hasher.update(authority_domain.origin_id.as_bytes()); + hasher.update(authority_domain.domain_id.as_bytes()); + hasher.update(&member_commitment); + hasher.update(&[disclosure_budget_tag(disclosure_budget)]); +} + +const fn disclosure_budget_tag(budget: DisclosureBudget) -> u8 { + match budget { + DisclosureBudget::Public => 0x01, + DisclosureBudget::AuthorityScoped => 0x02, + DisclosureBudget::CapabilityScoped => 0x03, + DisclosureBudget::HolderOnly => 0x04, + DisclosureBudget::ZeroKnowledge => 0x05, + } +} diff --git a/crates/warp-core/src/witness.rs b/crates/warp-core/src/witness.rs new file mode 100644 index 00000000..1dc30a14 --- /dev/null +++ b/crates/warp-core/src/witness.rs @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Witness receipt boundary and deterministic simulator fixtures. + +use blake3::Hasher; +use thiserror::Error; + +use crate::ident::Hash; + +const WITNESS_RECEIPT_DOMAIN: &[u8] = b"echo.witness.receipt.v1\0"; + +/// Witness receipt family. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum WitnessKind { + /// E1 local self-witness: deterministic integrity evidence only. + SelfWitness, + /// Independent signature-backed witness receipt. + SignedWitness, + /// Threshold-backed witness receipt. + ThresholdWitness, + /// Runtime attestation receipt. + RuntimeAttestation, + /// Replay trace receipt emitted by a replay witness backend. + ReplayTraceReceipt, + /// ZK verifier receipt. + ZkVerifierReceipt, + /// Vector-opening verifier receipt. + VectorOpeningReceipt, +} + +impl WitnessKind { + /// Stable wire tag for canonical witness receipt hashing. + #[must_use] + pub const fn canonical_tag(self) -> u8 { + match self { + Self::SelfWitness => 0x01, + Self::SignedWitness => 0x02, + Self::ThresholdWitness => 0x03, + Self::RuntimeAttestation => 0x04, + Self::ReplayTraceReceipt => 0x05, + Self::ZkVerifierReceipt => 0x06, + Self::VectorOpeningReceipt => 0x07, + } + } +} + +/// Attestation strength claimed by a witness receipt. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum WitnessAttestation { + /// Integrity-only evidence. This is not independent attestation. + IntegrityOnly, + /// A backend claims independent attestation. + IndependentAttestation, +} + +impl WitnessAttestation { + const fn canonical_tag(self) -> u8 { + match self { + Self::IntegrityOnly => 0x01, + Self::IndependentAttestation => 0x02, + } + } +} + +/// Compatibility rule governing witness receipt identity. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum WitnessCompatibilityRule { + /// Public stable v1 identity. Changes require migration handling. + StableV1, + /// E1 scaffolding identity. May change with an explicit compatibility note. + E1Scaffold, + /// Identity change requiring a named migration. + RequiresMigration { + /// Source compatibility version. + from: u32, + /// Target compatibility version. + to: u32, + }, +} + +impl WitnessCompatibilityRule { + fn hash_into(self, hasher: &mut Hasher) { + match self { + Self::StableV1 => { + hasher.update(&[0x01]); + } + Self::E1Scaffold => { + hasher.update(&[0x02]); + } + Self::RequiresMigration { from, to } => { + hasher.update(&[0x03]); + hasher.update(&from.to_le_bytes()); + hasher.update(&to.to_le_bytes()); + } + } + } +} + +/// Receipt returned by a witness backend. +/// +/// A receipt names the subject being witnessed, the evidence material digest, +/// the witness family, the attestation claim, and the compatibility rule that +/// governs the receipt identity. Self-witness receipts are integrity-only +/// scaffolding unless an external backend returns a stronger receipt. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WitnessReceipt { + /// Witness family. + kind: WitnessKind, + /// Digest of the subject being witnessed. + subject_digest: Hash, + /// Digest of the witness evidence material. + evidence_digest: Hash, + /// Compatibility rule governing receipt identity. + compatibility: WitnessCompatibilityRule, + /// Attestation strength claimed by this receipt. + attestation: WitnessAttestation, +} + +impl WitnessReceipt { + /// Creates a witness receipt with an explicit compatibility rule. + /// + /// # Errors + /// + /// Returns [`WitnessError::UnsupportedCompatibility`] or + /// [`WitnessError::UnsupportedAttestation`] when a self-witness receipt tries + /// to claim more than E1 integrity-only scaffolding. + pub fn new( + kind: WitnessKind, + subject_digest: Hash, + evidence_digest: Hash, + compatibility: WitnessCompatibilityRule, + attestation: WitnessAttestation, + ) -> Result { + if kind == WitnessKind::SelfWitness { + if compatibility != WitnessCompatibilityRule::E1Scaffold { + return Err(WitnessError::UnsupportedCompatibility { + kind, + compatibility, + }); + } + if attestation != WitnessAttestation::IntegrityOnly { + return Err(WitnessError::UnsupportedAttestation { kind, attestation }); + } + } + Ok(Self { + kind, + subject_digest, + evidence_digest, + compatibility, + attestation, + }) + } + + /// Creates an E1 self-witness receipt. + /// + /// The returned receipt claims only local integrity evidence, not + /// independent attestation. + #[must_use] + pub const fn self_witness(subject_digest: Hash, evidence_digest: Hash) -> Self { + Self { + kind: WitnessKind::SelfWitness, + subject_digest, + evidence_digest, + compatibility: WitnessCompatibilityRule::E1Scaffold, + attestation: WitnessAttestation::IntegrityOnly, + } + } + + /// Returns the witness family. + #[must_use] + pub const fn kind(self) -> WitnessKind { + self.kind + } + + /// Returns the digest of the subject being witnessed. + #[must_use] + pub const fn subject_digest(self) -> Hash { + self.subject_digest + } + + /// Returns the digest of the witness evidence material. + #[must_use] + pub const fn evidence_digest(self) -> Hash { + self.evidence_digest + } + + /// Returns the compatibility rule governing receipt identity. + #[must_use] + pub const fn compatibility(self) -> WitnessCompatibilityRule { + self.compatibility + } + + /// Returns the attestation strength claimed by this receipt. + #[must_use] + pub const fn attestation(self) -> WitnessAttestation { + self.attestation + } + + /// Returns the canonical receipt digest. + #[must_use] + pub fn digest(&self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(WITNESS_RECEIPT_DOMAIN); + hasher.update(&[self.kind.canonical_tag()]); + hasher.update(&self.subject_digest); + hasher.update(&self.evidence_digest); + self.compatibility.hash_into(&mut hasher); + hasher.update(&[self.attestation.canonical_tag()]); + hasher.finalize().into() + } +} + +/// Request submitted to a witness backend. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WitnessRequest { + /// Requested witness family. + pub kind: WitnessKind, + /// Digest of the subject being witnessed. + pub subject_digest: Hash, + /// Digest of the witness evidence material. + pub evidence_digest: Hash, + /// Compatibility rule the caller expects the receipt to bind. + pub compatibility: WitnessCompatibilityRule, +} + +impl WitnessRequest { + /// Creates a witness request with an explicit compatibility rule. + #[must_use] + pub const fn new( + kind: WitnessKind, + subject_digest: Hash, + evidence_digest: Hash, + compatibility: WitnessCompatibilityRule, + ) -> Self { + Self { + kind, + subject_digest, + evidence_digest, + compatibility, + } + } + + /// Creates an E1 self-witness request. + #[must_use] + pub const fn self_witness(subject_digest: Hash, evidence_digest: Hash) -> Self { + Self::new( + WitnessKind::SelfWitness, + subject_digest, + evidence_digest, + WitnessCompatibilityRule::E1Scaffold, + ) + } +} + +/// Future witness backend rejection code. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum WitnessRejectionCode { + /// A witness backend rejected the request without a narrower public reason. + Rejected, +} + +/// Structured witness backend failure. +#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)] +pub enum WitnessError { + /// No backend is wired for this witness kind. + #[error("{kind:?} witness receipts require a backend before admission")] + UnsupportedBackend { + /// Unsupported witness kind. + kind: WitnessKind, + }, + /// The requested compatibility rule is not valid for this witness kind. + #[error("{kind:?} witness receipts do not support compatibility {compatibility:?}")] + UnsupportedCompatibility { + /// Witness kind that rejected the compatibility rule. + kind: WitnessKind, + /// Unsupported compatibility rule. + compatibility: WitnessCompatibilityRule, + }, + /// The requested attestation strength is not valid for this witness kind. + #[error("{kind:?} witness receipts do not support attestation {attestation:?}")] + UnsupportedAttestation { + /// Witness kind that rejected the attestation strength. + kind: WitnessKind, + /// Unsupported attestation strength. + attestation: WitnessAttestation, + }, + /// A witness backend rejected the request. + #[error("{kind:?} witness backend rejected request: {reason:?}")] + BackendRejected { + /// Witness kind rejected by the backend. + kind: WitnessKind, + /// Backend rejection code. + reason: WitnessRejectionCode, + }, +} + +/// Verifier-shaped boundary for witness receipt backends. +pub trait WitnessBackend { + /// Verifies or witnesses the request and returns a typed receipt. + /// + /// # Errors + /// + /// Returns [`WitnessError`] when the backend is unsupported or rejects the + /// request. + fn verify(&self, request: &WitnessRequest) -> Result; +} + +/// Deterministic witness simulator fixture. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WitnessSimulatorFixture { + /// Emits E1 self-witness integrity-only receipts. + SelfWitness, + /// Emits signed-witness fixture receipts. + SignedWitnessFixture, + /// Emits threshold-witness fixture receipts. + ThresholdWitnessFixture, + /// Rejects every request with a typed backend rejection. + RejectedWitnessFixture, + /// Reports every request as unsupported. + UnsupportedWitnessFixture, +} + +/// Deterministic witness backend simulator. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WitnessBackendSimulator { + fixture: WitnessSimulatorFixture, +} + +impl WitnessBackendSimulator { + /// Creates a deterministic witness backend simulator. + #[must_use] + pub const fn new(fixture: WitnessSimulatorFixture) -> Self { + Self { fixture } + } + + /// Returns the fixture behavior used by this simulator. + #[must_use] + pub const fn fixture(self) -> WitnessSimulatorFixture { + self.fixture + } +} + +impl WitnessBackend for WitnessBackendSimulator { + fn verify(&self, request: &WitnessRequest) -> Result { + match self.fixture { + WitnessSimulatorFixture::SelfWitness if request.kind == WitnessKind::SelfWitness => { + if request.compatibility != WitnessCompatibilityRule::E1Scaffold { + return Err(WitnessError::UnsupportedCompatibility { + kind: request.kind, + compatibility: request.compatibility, + }); + } + Ok(WitnessReceipt::self_witness( + request.subject_digest, + request.evidence_digest, + )) + } + WitnessSimulatorFixture::SignedWitnessFixture + if request.kind == WitnessKind::SignedWitness => + { + Ok(WitnessReceipt::new( + request.kind, + request.subject_digest, + request.evidence_digest, + request.compatibility, + WitnessAttestation::IndependentAttestation, + )?) + } + WitnessSimulatorFixture::ThresholdWitnessFixture + if request.kind == WitnessKind::ThresholdWitness => + { + Ok(WitnessReceipt::new( + request.kind, + request.subject_digest, + request.evidence_digest, + request.compatibility, + WitnessAttestation::IndependentAttestation, + )?) + } + WitnessSimulatorFixture::RejectedWitnessFixture => Err(WitnessError::BackendRejected { + kind: request.kind, + reason: WitnessRejectionCode::Rejected, + }), + WitnessSimulatorFixture::UnsupportedWitnessFixture + | WitnessSimulatorFixture::SelfWitness + | WitnessSimulatorFixture::SignedWitnessFixture + | WitnessSimulatorFixture::ThresholdWitnessFixture => { + Err(WitnessError::UnsupportedBackend { kind: request.kind }) + } + } + } +} diff --git a/crates/warp-core/tests/plurality_law_public_api_tests.rs b/crates/warp-core/tests/plurality_law_public_api_tests.rs new file mode 100644 index 00000000..06383bcf --- /dev/null +++ b/crates/warp-core/tests/plurality_law_public_api_tests.rs @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS + +//! External-consumer plurality law public API checks. + +use warp_core::{ + AuthorityDomainId, AuthorityDomainRef, DisclosureBudget, OriginId, PluralityLawCard, + PluralityLawCardError, PluralityLawConcealment, PluralityLawEmission, + PluralityLawEvidencePosture, PluralityLawFamily, PluralityLawName, PluralityLawObstruction, + PluralityLawObstructionKind, PluralityLawReading, PluralityLawReadingError, PluralityLawRef, + PluralityLawRefError, PluralityLawRegistry, PluralityLawRegistryError, PluralityLawRequirement, + WitnessAttestation, WitnessCompatibilityRule, WitnessKind, WitnessReceipt, +}; + +fn law_name(byte: u8) -> PluralityLawName { + PluralityLawName::from_bytes([byte; 32]) +} + +fn authority_ref(byte: u8) -> AuthorityDomainRef { + AuthorityDomainRef::new( + OriginId::from_bytes([byte; 32]), + AuthorityDomainId::from_bytes([byte.wrapping_add(1); 32]), + ) +} + +fn law_ref(byte: u8, version: u32) -> Result { + PluralityLawRef::new(PluralityLawFamily::Settlement, law_name(byte), version) +} + +fn law_card(law_ref: PluralityLawRef) -> Result { + PluralityLawCard::new( + law_ref, + vec![ + PluralityLawRequirement::SupportPins, + PluralityLawRequirement::FrontierDigest, + PluralityLawRequirement::PostureFloor, + ], + vec![PluralityLawEmission::PluralArtifact], + vec![PluralityLawConcealment::SealedMemberSourceChain], + PluralityLawEvidencePosture::SelfWitnessIntegrityOnly, + ) +} + +#[test] +fn public_plurality_law_registry_registers_machine_readable_cards( +) -> Result<(), Box> { + let law_ref = law_ref(0x51, 1)?; + let card = law_card(law_ref)?; + let mut registry = PluralityLawRegistry::new(); + + registry.register(card.clone())?; + + assert_eq!(registry.card(&law_ref), Some(&card)); + assert_eq!(card.law_ref(), law_ref); + assert_eq!(card.version(), 1); + assert!(card + .requires() + .contains(&PluralityLawRequirement::FrontierDigest)); + assert_eq!( + registry.register(card), + Err(PluralityLawRegistryError::DuplicateLaw { law_ref }) + ); + Ok(()) +} + +#[test] +fn public_plurality_law_ref_requires_name_and_version() { + assert_eq!( + PluralityLawRef::new(PluralityLawFamily::Settlement, law_name(0x00), 1), + Err(PluralityLawRefError::EmptyName) + ); + assert_eq!( + PluralityLawRef::settlement_policy([0; 32]), + Err(PluralityLawRefError::EmptyName) + ); + assert_eq!( + PluralityLawRef::collapse_policy([0; 32]), + Err(PluralityLawRefError::EmptyName) + ); + assert_eq!( + PluralityLawRef::collapse_policy([0x57; 32]).map(PluralityLawRef::family), + Ok(PluralityLawFamily::Collapse) + ); + assert_eq!( + PluralityLawRef::new(PluralityLawFamily::Settlement, law_name(0x55), 0), + Err(PluralityLawRefError::ZeroVersion) + ); +} + +#[test] +fn public_plurality_law_reading_identity_binds_law_name_and_version( +) -> Result<(), Box> { + let witness = WitnessReceipt::self_witness([0xAA; 32], [0xBB; 32]); + let v1 = PluralityLawReading::new( + law_ref(0x52, 1)?, + witness.subject_digest(), + witness, + DisclosureBudget::AuthorityScoped, + )?; + let v2 = PluralityLawReading::new( + law_ref(0x52, 2)?, + witness.subject_digest(), + witness, + DisclosureBudget::AuthorityScoped, + )?; + + assert_eq!(v1.law_ref().version(), 1); + assert_eq!(v2.law_ref().version(), 2); + assert_ne!(v1.digest(), v2.digest()); + Ok(()) +} + +#[test] +fn public_plurality_law_reading_does_not_promote_integrity_only_receipts( +) -> Result<(), Box> { + let support_digest = [0xAA; 32]; + let witness = WitnessReceipt::new( + WitnessKind::SignedWitness, + support_digest, + [0xBB; 32], + WitnessCompatibilityRule::StableV1, + WitnessAttestation::IntegrityOnly, + )?; + let reading = PluralityLawReading::new( + law_ref(0x56, 1)?, + support_digest, + witness, + DisclosureBudget::AuthorityScoped, + )?; + + assert_eq!( + reading.evidence_posture(), + PluralityLawEvidencePosture::SelfWitnessIntegrityOnly + ); + Ok(()) +} + +#[test] +fn public_plurality_law_reading_rejects_unbound_witness_receipt( +) -> Result<(), Box> { + let witness = WitnessReceipt::self_witness([0xAA; 32], [0xBB; 32]); + let support_digest = [0xCC; 32]; + + assert_eq!( + PluralityLawReading::new( + law_ref(0x58, 1)?, + support_digest, + witness, + DisclosureBudget::AuthorityScoped, + ), + Err(PluralityLawReadingError::WitnessSubjectMismatch { + expected: support_digest, + actual: witness.subject_digest(), + }) + ); + Ok(()) +} + +#[test] +fn public_adapter_provided_laws_route_through_authority_without_app_nouns( +) -> Result<(), Box> { + let authority = authority_ref(0xA0); + let law_ref = PluralityLawRef::new( + PluralityLawFamily::adapter_provided(authority), + law_name(0x53), + 1, + )?; + let mut registry = PluralityLawRegistry::new(); + registry.register(law_card(law_ref)?)?; + + assert_eq!( + registry.authorize(law_ref, None), + Err(PluralityLawObstruction::new( + PluralityLawObstructionKind::UnauthorizedLaw, + law_ref, + None, + )) + ); + assert!(registry.authorize(law_ref, Some(authority)).is_ok()); + Ok(()) +} + +#[test] +fn public_unsupported_law_execution_yields_typed_obstruction( +) -> Result<(), Box> { + let registry = PluralityLawRegistry::new(); + let law_ref = law_ref(0x54, 1)?; + + assert_eq!( + registry.authorize(law_ref, Some(authority_ref(0xB0))), + Err(PluralityLawObstruction::new( + PluralityLawObstructionKind::UnsupportedLaw, + law_ref, + Some(authority_ref(0xB0)), + )) + ); + Ok(()) +} diff --git a/crates/warp-core/tests/witness_public_api_tests.rs b/crates/warp-core/tests/witness_public_api_tests.rs new file mode 100644 index 00000000..d022daa9 --- /dev/null +++ b/crates/warp-core/tests/witness_public_api_tests.rs @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS + +//! External-consumer witness receipt public API checks. + +use warp_core::{ + AuthorityDomainId, AuthorityDomainRef, BraidCoordinate, DisclosureBudget, OriginId, + PresentationPurpose, SealedMembershipPresentation, SealedMembershipPresentationError, + WitnessAttestation, WitnessBackend, WitnessBackendSimulator, WitnessCompatibilityRule, + WitnessError, WitnessKind, WitnessReceipt, WitnessRejectionCode, WitnessRequest, + WitnessSimulatorFixture, +}; + +fn subject_digest() -> [u8; 32] { + [0xA1; 32] +} + +fn evidence_digest() -> [u8; 32] { + [0xE1; 32] +} + +fn authority_ref() -> AuthorityDomainRef { + AuthorityDomainRef::new( + OriginId::from_bytes([0x10; 32]), + AuthorityDomainId::from_bytes([0x20; 32]), + ) +} + +#[test] +fn public_witness_backend_reports_unsupported_kind_as_typed_error() { + let backend = WitnessBackendSimulator::new(WitnessSimulatorFixture::UnsupportedWitnessFixture); + let request = WitnessRequest::new( + WitnessKind::ZkVerifierReceipt, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::StableV1, + ); + + assert_eq!( + backend.verify(&request), + Err(WitnessError::UnsupportedBackend { + kind: WitnessKind::ZkVerifierReceipt, + }) + ); +} + +#[test] +fn public_witness_simulator_returns_deterministic_fixture_receipts() -> Result<(), WitnessError> { + let backend = WitnessBackendSimulator::new(WitnessSimulatorFixture::SignedWitnessFixture); + let request = WitnessRequest::new( + WitnessKind::SignedWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::StableV1, + ); + + let first = backend.verify(&request)?; + let second = backend.verify(&request)?; + + assert_eq!(first, second); + assert_eq!(first.kind(), WitnessKind::SignedWitness); + assert_eq!( + first.attestation(), + WitnessAttestation::IndependentAttestation + ); + assert_eq!(first.compatibility(), WitnessCompatibilityRule::StableV1); + assert_eq!(first.digest(), second.digest()); + Ok(()) +} + +#[test] +fn public_rejected_witness_fixture_is_typed() { + let backend = WitnessBackendSimulator::new(WitnessSimulatorFixture::RejectedWitnessFixture); + let request = WitnessRequest::new( + WitnessKind::ThresholdWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::StableV1, + ); + + assert_eq!( + backend.verify(&request), + Err(WitnessError::BackendRejected { + kind: WitnessKind::ThresholdWitness, + reason: WitnessRejectionCode::Rejected, + }) + ); +} + +#[test] +fn public_self_witness_fixture_rejects_stable_identity_requests() { + let backend = WitnessBackendSimulator::new(WitnessSimulatorFixture::SelfWitness); + let request = WitnessRequest::new( + WitnessKind::SelfWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::StableV1, + ); + + assert_eq!( + backend.verify(&request), + Err(WitnessError::UnsupportedCompatibility { + kind: WitnessKind::SelfWitness, + compatibility: WitnessCompatibilityRule::StableV1, + }) + ); +} + +#[test] +fn public_witness_receipt_rejects_self_witness_overclaims() { + assert_eq!( + WitnessReceipt::new( + WitnessKind::SelfWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::StableV1, + WitnessAttestation::IntegrityOnly, + ), + Err(WitnessError::UnsupportedCompatibility { + kind: WitnessKind::SelfWitness, + compatibility: WitnessCompatibilityRule::StableV1, + }) + ); + assert_eq!( + WitnessReceipt::new( + WitnessKind::SelfWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::E1Scaffold, + WitnessAttestation::IndependentAttestation, + ), + Err(WitnessError::UnsupportedAttestation { + kind: WitnessKind::SelfWitness, + attestation: WitnessAttestation::IndependentAttestation, + }) + ); +} + +#[test] +fn public_witness_receipt_identity_binds_compatibility_rule() -> Result<(), WitnessError> { + let scaffold = WitnessReceipt::new( + WitnessKind::SignedWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::E1Scaffold, + WitnessAttestation::IndependentAttestation, + )?; + let stable = WitnessReceipt::new( + WitnessKind::SignedWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::StableV1, + WitnessAttestation::IndependentAttestation, + )?; + + assert_ne!(scaffold.digest(), stable.digest()); + Ok(()) +} + +#[test] +fn public_sealed_membership_presentation_rejects_unbound_receipts() { + let purpose = PresentationPurpose::new([0x44; 32]); + let coordinate = BraidCoordinate([0xBC; 32]); + let authority = authority_ref(); + let member_commitment = [0xA5; 32]; + let disclosure_budget = DisclosureBudget::CapabilityScoped; + let expected = SealedMembershipPresentation::witness_subject_digest( + coordinate, + purpose, + authority, + member_commitment, + disclosure_budget, + ); + let receipt = WitnessReceipt::self_witness([0xDE; 32], evidence_digest()); + + assert_eq!( + SealedMembershipPresentation::new( + coordinate, + purpose, + authority, + member_commitment, + receipt, + disclosure_budget, + ), + Err(SealedMembershipPresentationError::WitnessSubjectMismatch { + expected, + actual: [0xDE; 32], + }) + ); +} + +#[test] +fn public_sealed_membership_presentation_rejects_wrong_evidence_digest() { + let purpose = PresentationPurpose::new([0x44; 32]); + let coordinate = BraidCoordinate([0xBC; 32]); + let authority = authority_ref(); + let member_commitment = [0xA5; 32]; + let disclosure_budget = DisclosureBudget::CapabilityScoped; + let subject = SealedMembershipPresentation::witness_subject_digest( + coordinate, + purpose, + authority, + member_commitment, + disclosure_budget, + ); + let expected = SealedMembershipPresentation::witness_evidence_digest( + coordinate, + purpose, + authority, + member_commitment, + disclosure_budget, + ); + let wrong_evidence = [0xEF; 32]; + assert_ne!(expected, wrong_evidence); + let receipt = WitnessReceipt::self_witness(subject, wrong_evidence); + + assert_eq!( + SealedMembershipPresentation::new( + coordinate, + purpose, + authority, + member_commitment, + receipt, + disclosure_budget, + ), + Err(SealedMembershipPresentationError::WitnessEvidenceMismatch { + expected, + actual: wrong_evidence, + }) + ); +} + +#[test] +fn public_sealed_membership_presentation_uses_generic_purpose_and_budget( +) -> Result<(), SealedMembershipPresentationError> { + let purpose = PresentationPurpose::new([0x44; 32]); + let coordinate = BraidCoordinate([0xBC; 32]); + let authority = authority_ref(); + let member_commitment = [0xA5; 32]; + let disclosure_budget = DisclosureBudget::CapabilityScoped; + let subject = SealedMembershipPresentation::witness_subject_digest( + coordinate, + purpose, + authority, + member_commitment, + disclosure_budget, + ); + let evidence = SealedMembershipPresentation::witness_evidence_digest( + coordinate, + purpose, + authority, + member_commitment, + disclosure_budget, + ); + let receipt = WitnessReceipt::self_witness(subject, evidence); + let presentation = SealedMembershipPresentation::new( + coordinate, + purpose, + authority, + member_commitment, + receipt, + disclosure_budget, + )?; + + assert_eq!(presentation.braid_coordinate(), coordinate); + assert_eq!(presentation.purpose(), purpose); + assert_eq!(presentation.purpose().purpose_id(), [0x44; 32]); + assert_eq!(presentation.authority_domain(), authority); + assert_eq!(presentation.member_commitment(), member_commitment); + assert_eq!(presentation.disclosure_budget(), disclosure_budget); + assert_eq!(presentation.witness_receipt(), receipt); + Ok(()) +} diff --git a/docs/design/braids-and-strands-hardening/goalpost-04-witness-receipts-and-sealed-capabilities.md b/docs/design/braids-and-strands-hardening/goalpost-04-witness-receipts-and-sealed-capabilities.md index 9ac60011..fdab53b9 100644 --- a/docs/design/braids-and-strands-hardening/goalpost-04-witness-receipts-and-sealed-capabilities.md +++ b/docs/design/braids-and-strands-hardening/goalpost-04-witness-receipts-and-sealed-capabilities.md @@ -3,7 +3,7 @@ # Goalpost 4: Witness Receipts And Sealed Capabilities -Status: planned. +Status: implemented. Roadmap: [`../braids-and-strands-roadmap.md`](../braids-and-strands-roadmap.md) @@ -53,6 +53,52 @@ This goalpost does not include: - sealed membership before historical membership and salt vectors exist; - treating self-witness as independent attestation. +## Implementation Design + +`WitnessReceipt` names the witness boundary without requiring a real external +backend. Receipt identity binds: + +```text +WitnessKind ++ subject digest ++ evidence digest ++ WitnessCompatibilityRule ++ WitnessAttestation +``` + +`WitnessKind` reserves the families Echo needs before they are implemented: +self-witness, signed witness, threshold witness, runtime attestation, +replay-trace receipt, ZK verifier receipt, and vector-opening receipt. +`WitnessBackend` is a verifier-shaped boundary: callers submit a +`WitnessRequest` and receive either a typed `WitnessReceipt` or a typed +`WitnessError`. + +The deterministic `WitnessBackendSimulator` hardens the boundary before real +backends exist. Its fixtures cover self-witness, signed-witness, +threshold-witness, rejected, and unsupported outcomes. Unsupported witness +kinds return `WitnessError::UnsupportedBackend`; rejected requests return +`WitnessError::BackendRejected`. + +`WitnessCompatibilityRule` is explicit in the receipt digest. E1 self-witness +receipts use `E1Scaffold`; stable external receipts can use `StableV1`; future +identity changes must name `RequiresMigration`. The self-witness simulator +rejects non-`E1Scaffold` compatibility requests with +`WitnessError::UnsupportedCompatibility` so deterministic local scaffolding +cannot accidentally claim stable public receipt identity. + +`SealedMembershipPresentation` is purpose-bound and generic. It carries a +`PresentationPurpose` digest rather than application-domain purpose nouns, a +braid coordinate, authority domain, blinded member commitment, witness receipt, +and `DisclosureBudget`. Its constructor validates that the witness receipt +subject and evidence digests are the canonical presentation digests over those +fields, and the fields are read-only after construction. + +`BraidShellAudit` now carries a typed `WitnessReceipt` and labels each member +fact with a disclosure budget. Revealed member references report `Public`; +sealed member references report `AuthorityScoped`. This reports what was +lawfully visible without reopening member strand histories or treating +self-witness as independent attestation. + ## Slices | Slice | Work | Witness | @@ -67,6 +113,7 @@ This goalpost does not include: - Unsupported witness kinds fail as typed unsupported-backend outcomes. - Simulator fixtures harden witness behavior before real backends exist. +- Self-witness fixtures cannot mint `StableV1` receipts. - `PresentationPurpose` remains a generic capability purpose, not an application-domain enum. - Replay records what was proven, what remained sealed, and which disclosure diff --git a/docs/design/braids-and-strands-hardening/goalpost-05-named-plurality-laws.md b/docs/design/braids-and-strands-hardening/goalpost-05-named-plurality-laws.md index ecbf205d..2201f9df 100644 --- a/docs/design/braids-and-strands-hardening/goalpost-05-named-plurality-laws.md +++ b/docs/design/braids-and-strands-hardening/goalpost-05-named-plurality-laws.md @@ -3,7 +3,7 @@ # Goalpost 5: Named Plurality Laws -Status: planned. +Status: implemented. Roadmap: [`../braids-and-strands-roadmap.md`](../braids-and-strands-roadmap.md) @@ -51,6 +51,49 @@ This goalpost does not include: - executing laws before replay and witness boundaries exist; - collapsing braided strands into merge semantics. +## Implementation Design + +`PluralityLawRef` is the named law reference: + +```text +PluralityLawFamily ++ PluralityLawName ++ version +``` + +Law names cannot be the all-zero digest, and law versions start at 1. Existing +braid settlement policy ids map through the fallible +`PluralityLawRef::settlement_policy(...)` constructor, preserving the current +retained policy identity while making the law family and version explicit in +replay. Braid shell assembly and validation reject all-zero policy ids before a +retained shell can claim a named law reading. Collapse-derived shells map their +`collapse_policy` id through `PluralityLawRef::collapse_policy(...)`, so replay +and audit report collapse readings as `PluralityLawFamily::Collapse` instead of +settlement-law readings. + +`PluralityLawFamily` is core-generic: settlement, collapse, +conflict-preserving, quorum, authority, and adapter-provided. Adapter-provided +families are scoped by `AuthorityDomainRef`, so Echo core can route +domain-specific laws without importing application-domain law nouns. + +`PluralityLawCard` is the machine-readable Law Card. It binds a law reference, +required support/evidence facts, emitted artifact or reading classes, concealed +material classes, and `PluralityLawEvidencePosture`. Requirements, emissions, +and concealments are sorted and deduplicated before card identity is computed, +so caller vector order is not part of law identity. + +`PluralityLawRegistry` registers Law Cards deterministically by +`PluralityLawRef`. Duplicate registration returns +`PluralityLawRegistryError::DuplicateLaw`. Execution authorization returns +`PluralityLawAuthorization` for registered laws and typed +`PluralityLawObstruction` for unsupported or unauthorized execution. + +`PluralityLawReading` binds the law reference, retained support digest, +witness receipt, evidence posture, and disclosure budget into a witnessed +reading digest. `BraidShellReplay` now carries the settlement `law_ref`, and +`BraidShellAudit` carries the full `law_reading` so retained braid readings +state which named law interpreted plurality. + ## Slices | Slice | Work | Witness | diff --git a/docs/design/braids-and-strands-roadmap.md b/docs/design/braids-and-strands-roadmap.md index db95af2c..d27e7065 100644 --- a/docs/design/braids-and-strands-roadmap.md +++ b/docs/design/braids-and-strands-roadmap.md @@ -103,12 +103,12 @@ Design: Design: [`goalpost-04-witness-receipts-and-sealed-capabilities.md`](braids-and-strands-hardening/goalpost-04-witness-receipts-and-sealed-capabilities.md) -- [ ] GP4-S1: Define `WitnessReceipt`, `WitnessKind`, and `WitnessBackend`. -- [ ] GP4-S2: Add deterministic witness simulator fixtures for supported, +- [x] GP4-S1: Define `WitnessReceipt`, `WitnessKind`, and `WitnessBackend`. +- [x] GP4-S2: Add deterministic witness simulator fixtures for supported, rejected, and unsupported outcomes. -- [ ] GP4-S3: Bind witness identity only through explicit compatibility rules. -- [ ] GP4-S4: Design purpose-bound sealed membership presentations. -- [ ] GP4-S5: Add disclosure budget labels and replay wording for sealed +- [x] GP4-S3: Bind witness identity only through explicit compatibility rules. +- [x] GP4-S4: Design purpose-bound sealed membership presentations. +- [x] GP4-S5: Add disclosure budget labels and replay wording for sealed membership. ### Goalpost 5: Named Plurality Laws @@ -116,12 +116,12 @@ Design: Design: [`goalpost-05-named-plurality-laws.md`](braids-and-strands-hardening/goalpost-05-named-plurality-laws.md) -- [ ] GP5-S1: Define the core plurality law registry shape. -- [ ] GP5-S2: Add machine-readable Law Cards. -- [ ] GP5-S3: Bind law name and version into witnessed readings. -- [ ] GP5-S4: Route adapter-provided law families without application nouns in +- [x] GP5-S1: Define the core plurality law registry shape. +- [x] GP5-S2: Add machine-readable Law Cards. +- [x] GP5-S3: Bind law name and version into witnessed readings. +- [x] GP5-S4: Route adapter-provided law families without application nouns in Echo core. -- [ ] GP5-S5: Add obstruction evidence for unsupported or unauthorized law +- [x] GP5-S5: Add obstruction evidence for unsupported or unauthorized law execution. ## North Star @@ -548,7 +548,7 @@ Work: 6. Reserve a migration hook shape: ```rust - pub enum CompatibilityRule { + pub enum WitnessCompatibilityRule { StableV1, E1Scaffold, RequiresMigration { from: u32, to: u32 },