From 05411d9921ffd1de2fbe205a21589c8cd6b97316 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 16 Jun 2026 18:11:53 -0700 Subject: [PATCH 01/14] feat(warp-core): add witness receipt boundary --- CHANGELOG.md | 6 + crates/warp-core/src/braid_shell.rs | 55 +++ crates/warp-core/src/lib.rs | 10 + crates/warp-core/src/sealed_membership.rs | 89 +++++ crates/warp-core/src/witness.rs | 327 ++++++++++++++++++ .../tests/witness_public_api_tests.rs | 129 +++++++ ...itness-receipts-and-sealed-capabilities.md | 43 ++- docs/design/braids-and-strands-roadmap.md | 12 +- 8 files changed, 664 insertions(+), 7 deletions(-) create mode 100644 crates/warp-core/src/sealed_membership.rs create mode 100644 crates/warp-core/src/witness.rs create mode 100644 crates/warp-core/tests/witness_public_api_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index eb55d6ce..97db6582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,12 @@ - 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. - `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..c5fd5bc8 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -25,7 +25,9 @@ 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"; @@ -977,6 +979,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. @@ -1002,6 +1006,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. @@ -1092,15 +1098,24 @@ 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: WitnessReceipt::self_witness(shell.digest, shell.witness_digest), }) } +const fn member_disclosure_budget(member_ref: BraidMemberRef) -> DisclosureBudget { + match member_ref { + BraidMemberRef::Revealed(_) => DisclosureBudget::Public, + BraidMemberRef::Sealed { .. } => DisclosureBudget::AuthorityScoped, + } +} + fn validated_shell_for_replay<'a>( digest: &Hash, records: &'a dyn BraidShellRecords, @@ -1563,6 +1578,8 @@ mod tests { #[test] fn replay_audit_reports_member_proof_support_frontier_and_witness_facts() { 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( @@ -1618,6 +1635,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 +1652,43 @@ 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 + ); } #[test] diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 34c9154e..20501da4 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -149,6 +149,7 @@ mod revelation; mod rule; mod sandbox; mod scheduler; +mod sealed_membership; mod settlement; mod snapshot; mod snapshot_accum; @@ -159,6 +160,7 @@ mod tick_patch; mod trusted_runtime_host; mod tx; mod warp_state; +mod witness; mod witnessed_suffix; #[cfg(test)] mod witnessed_suffix_tests; @@ -253,12 +255,20 @@ pub use playback::{ pub use retained_evidence::{ RetainedEvidenceCoordinate, RetainedEvidencePosture, RetainedEvidenceRef, RetainedEvidenceRole, }; +// --- Sealed membership capability types --- +pub use sealed_membership::{DisclosureBudget, PresentationPurpose, SealedMembershipPresentation}; // --- 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/sealed_membership.rs b/crates/warp-core/src/sealed_membership.rs new file mode 100644 index 00000000..01498273 --- /dev/null +++ b/crates/warp-core/src/sealed_membership.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Purpose-bound sealed membership presentation vocabulary. + +use crate::braid_shell::BraidCoordinate; +use crate::ident::Hash; +use crate::revelation::AuthorityDomainRef; +use crate::witness::WitnessReceipt; + +/// 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 + } +} + +/// 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. + pub braid_coordinate: BraidCoordinate, + /// Generic capability purpose for the presentation. + pub purpose: PresentationPurpose, + /// Authority domain under which the sealed member commitment is meaningful. + pub authority_domain: AuthorityDomainRef, + /// Blinded member commitment presented for the purpose. + pub member_commitment: Hash, + /// Witness receipt supporting this presentation. + pub witness_receipt: WitnessReceipt, + /// Disclosure budget governing the presentation. + pub disclosure_budget: DisclosureBudget, +} + +impl SealedMembershipPresentation { + /// Creates a purpose-bound sealed membership presentation. + #[must_use] + pub const fn new( + braid_coordinate: BraidCoordinate, + purpose: PresentationPurpose, + authority_domain: AuthorityDomainRef, + member_commitment: Hash, + witness_receipt: WitnessReceipt, + disclosure_budget: DisclosureBudget, + ) -> Self { + Self { + braid_coordinate, + purpose, + authority_domain, + member_commitment, + witness_receipt, + disclosure_budget, + } + } +} diff --git a/crates/warp-core/src/witness.rs b/crates/warp-core/src/witness.rs new file mode 100644 index 00000000..a3c117a8 --- /dev/null +++ b/crates/warp-core/src/witness.rs @@ -0,0 +1,327 @@ +// 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. + 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 governing receipt identity. + pub compatibility: WitnessCompatibilityRule, + /// Attestation strength claimed by this receipt. + pub attestation: WitnessAttestation, +} + +impl WitnessReceipt { + /// Creates a witness receipt with an explicit compatibility rule. + #[must_use] + pub const fn new( + kind: WitnessKind, + subject_digest: Hash, + evidence_digest: Hash, + compatibility: WitnessCompatibilityRule, + attestation: WitnessAttestation, + ) -> Self { + 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::new( + WitnessKind::SelfWitness, + subject_digest, + evidence_digest, + WitnessCompatibilityRule::E1Scaffold, + WitnessAttestation::IntegrityOnly, + ) + } + + /// 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, + }, + /// 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 => { + Ok(WitnessReceipt::new( + request.kind, + request.subject_digest, + request.evidence_digest, + request.compatibility, + WitnessAttestation::IntegrityOnly, + )) + } + 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/witness_public_api_tests.rs b/crates/warp-core/tests/witness_public_api_tests.rs new file mode 100644 index 00000000..535330cc --- /dev/null +++ b/crates/warp-core/tests/witness_public_api_tests.rs @@ -0,0 +1,129 @@ +// 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, 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_witness_receipt_identity_binds_compatibility_rule() { + let scaffold = WitnessReceipt::new( + WitnessKind::SelfWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::E1Scaffold, + WitnessAttestation::IntegrityOnly, + ); + let stable = WitnessReceipt::new( + WitnessKind::SelfWitness, + subject_digest(), + evidence_digest(), + WitnessCompatibilityRule::StableV1, + WitnessAttestation::IntegrityOnly, + ); + + assert_ne!(scaffold.digest(), stable.digest()); +} + +#[test] +fn public_sealed_membership_presentation_uses_generic_purpose_and_budget() { + let purpose = PresentationPurpose::new([0x44; 32]); + let receipt = WitnessReceipt::self_witness(subject_digest(), evidence_digest()); + let presentation = SealedMembershipPresentation::new( + BraidCoordinate([0xBC; 32]), + purpose, + authority_ref(), + [0xA5; 32], + receipt, + DisclosureBudget::CapabilityScoped, + ); + + assert_eq!(presentation.purpose, purpose); + assert_eq!(presentation.purpose.purpose_id(), [0x44; 32]); + assert_eq!( + presentation.disclosure_budget, + DisclosureBudget::CapabilityScoped + ); + assert_eq!(presentation.witness_receipt, receipt); +} 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..77695574 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,47 @@ 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`. + +`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`. + +`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 | diff --git a/docs/design/braids-and-strands-roadmap.md b/docs/design/braids-and-strands-roadmap.md index db95af2c..73c1cba1 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 @@ -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 }, From 2924072588850767d4b361c77cbafb46c2f7bb48 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 16 Jun 2026 18:27:34 -0700 Subject: [PATCH 02/14] fix(warp-core): keep self witness scaffolded --- CHANGELOG.md | 4 +++- crates/warp-core/src/witness.rs | 19 +++++++++++++++---- .../tests/witness_public_api_tests.rs | 19 +++++++++++++++++++ ...itness-receipts-and-sealed-capabilities.md | 6 +++++- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97db6582..e23a4960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,9 @@ 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. + 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. - `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/witness.rs b/crates/warp-core/src/witness.rs index a3c117a8..185f648e 100644 --- a/crates/warp-core/src/witness.rs +++ b/crates/warp-core/src/witness.rs @@ -222,6 +222,14 @@ pub enum WitnessError { /// 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, + }, /// A witness backend rejected the request. #[error("{kind:?} witness backend rejected request: {reason:?}")] BackendRejected { @@ -282,12 +290,15 @@ impl WitnessBackend for WitnessBackendSimulator { fn verify(&self, request: &WitnessRequest) -> Result { match self.fixture { WitnessSimulatorFixture::SelfWitness if request.kind == WitnessKind::SelfWitness => { - Ok(WitnessReceipt::new( - request.kind, + 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, - request.compatibility, - WitnessAttestation::IntegrityOnly, )) } WitnessSimulatorFixture::SignedWitnessFixture diff --git a/crates/warp-core/tests/witness_public_api_tests.rs b/crates/warp-core/tests/witness_public_api_tests.rs index 535330cc..66192c8d 100644 --- a/crates/warp-core/tests/witness_public_api_tests.rs +++ b/crates/warp-core/tests/witness_public_api_tests.rs @@ -86,6 +86,25 @@ fn public_rejected_witness_fixture_is_typed() { ); } +#[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_identity_binds_compatibility_rule() { let scaffold = WitnessReceipt::new( 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 77695574..f94e0640 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 @@ -81,7 +81,10 @@ kinds return `WitnessError::UnsupportedBackend`; rejected requests return `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`. +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 @@ -108,6 +111,7 @@ self-witness as independent attestation. - 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 From f190c24bf270b53bcd275d4f2947eb4dfcd8670b Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 16 Jun 2026 18:40:47 -0700 Subject: [PATCH 03/14] feat(warp-core): add named plurality laws --- CHANGELOG.md | 5 + crates/warp-core/src/braid_shell.rs | 50 +- crates/warp-core/src/lib.rs | 8 + crates/warp-core/src/plurality_law.rs | 684 ++++++++++++++++++ .../tests/plurality_law_public_api_tests.rs | 127 ++++ .../goalpost-05-named-plurality-laws.md | 39 +- docs/design/braids-and-strands-roadmap.md | 10 +- 7 files changed, 916 insertions(+), 7 deletions(-) create mode 100644 crates/warp-core/src/plurality_law.rs create mode 100644 crates/warp-core/tests/plurality_law_public_api_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e23a4960..bd6fe8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,11 @@ 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. +- `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. - `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 c5fd5bc8..5f55d70e 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -21,6 +21,7 @@ use blake3::Hasher; use crate::admission::AdmissionOutcomeKind; use crate::ident::Hash; +use crate::plurality_law::{PluralityLawReading, PluralityLawRef}; use crate::provenance_store::ProvenanceRef; use crate::revelation::{ shell_posture_obstruction, AuthorityDomainRef, CausalPosture, PostureObstruction, WitnessDigest, @@ -924,6 +925,8 @@ pub struct BraidShellReplay { pub member_verdicts: Vec<(BraidMemberRef, MemberVerdict)>, /// Settlement policy identity the act ran under. pub policy_id: Hash, + /// Named settlement law that interpreted retained plurality. + pub law_ref: PluralityLawRef, /// Witness digest binding the act. pub witness_digest: Hash, /// Revelation posture of the shell. @@ -994,6 +997,8 @@ pub struct BraidShellAudit { pub outcome_kind: AdmissionOutcomeKind, /// Settlement 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. @@ -1027,6 +1032,7 @@ pub fn replay_braid_shell( records: &dyn BraidShellRecords, ) -> Result { let shell = validated_shell_for_replay(digest, records)?; + let law_ref = PluralityLawRef::settlement_policy(shell.policy_id); Ok(BraidShellReplay { outcome_kind: shell.outcome_kind(), member_verdicts: shell @@ -1035,6 +1041,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, }) @@ -1072,12 +1079,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 = PluralityLawRef::settlement_policy(shell.policy_id); + 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 @@ -1105,7 +1121,7 @@ pub fn audit_braid_shell( witness_posture: BraidWitnessPosture::SelfWitnessIntegrityOnly { digest: shell.witness_digest, }, - witness_receipt: WitnessReceipt::self_witness(shell.digest, shell.witness_digest), + witness_receipt, }) } @@ -1116,6 +1132,18 @@ const fn member_disclosure_budget(member_ref: BraidMemberRef) -> DisclosureBudge } } +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 validated_shell_for_replay<'a>( digest: &Hash, records: &'a dyn BraidShellRecords, @@ -1556,6 +1584,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), @@ -1572,11 +1602,16 @@ 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!( + 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}; @@ -1620,6 +1655,19 @@ mod tests { assert_eq!(audit.shell_digest, digest); assert_eq!(audit.outcome_kind, AdmissionOutcomeKind::Plural); + assert_eq!( + 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]); diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 20501da4..8b38d0c2 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; @@ -245,6 +246,13 @@ 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, PluralityLawRef, + PluralityLawRefError, PluralityLawRegistry, PluralityLawRegistryError, PluralityLawRequirement, +}; // --- Cursor types --- pub use contract_obstruction::{ ContractObstruction, ContractObstructionKind, ContractObstructionSubject, diff --git a/crates/warp-core/src/plurality_law.rs b/crates/warp-core/src/plurality_law.rs new file mode 100644 index 00000000..55d45733 --- /dev/null +++ b/crates/warp-core/src/plurality_law.rs @@ -0,0 +1,684 @@ +// 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, WitnessKind, 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 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::ZeroVersion`] when `version` is zero. + pub const fn new( + family: PluralityLawFamily, + name: PluralityLawName, + version: u32, + ) -> Result { + if version == 0 { + return Err(PluralityLawRefError::ZeroVersion); + } + Ok(Self { + family, + name, + version, + }) + } + + /// Constructs the v1 settlement-law reference for an existing policy id. + #[must_use] + pub const fn settlement_policy(policy_id: Hash) -> Self { + Self { + family: PluralityLawFamily::Settlement, + name: PluralityLawName::from_bytes(policy_id), + version: 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 { + /// E1 local self-witness integrity evidence only. + 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, +} + +impl PluralityLawReading { + /// Constructs a witnessed plurality law reading. + #[must_use] + pub fn new( + law_ref: PluralityLawRef, + support_digest: Hash, + witness_receipt: WitnessReceipt, + disclosure_budget: DisclosureBudget, + ) -> Self { + let evidence_posture = evidence_posture_for(witness_receipt); + 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.kind, receipt.attestation) { + (WitnessKind::SelfWitness, WitnessAttestation::IntegrityOnly) => { + PluralityLawEvidencePosture::SelfWitnessIntegrityOnly + } + _ => 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 Iterator) { + let tags: Vec = tags.collect(); + hash_len(hasher, tags.len()); + hasher.update(&tags); +} + +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/tests/plurality_law_public_api_tests.rs b/crates/warp-core/tests/plurality_law_public_api_tests.rs new file mode 100644 index 00000000..53df66b3 --- /dev/null +++ b/crates/warp-core/tests/plurality_law_public_api_tests.rs @@ -0,0 +1,127 @@ +// 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, PluralityLawRef, PluralityLawRefError, + PluralityLawRegistry, PluralityLawRegistryError, PluralityLawRequirement, 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_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)?, + [0xCC; 32], + witness, + DisclosureBudget::AuthorityScoped, + ); + let v2 = PluralityLawReading::new( + law_ref(0x52, 2)?, + [0xCC; 32], + 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_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/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..6ce29a1e 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,43 @@ 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 versions start at 1. Existing braid settlement policy ids map into +`PluralityLawRef::settlement_policy(...)`, preserving the current retained +policy identity while making the law family and version explicit in replay. + +`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 73c1cba1..d27e7065 100644 --- a/docs/design/braids-and-strands-roadmap.md +++ b/docs/design/braids-and-strands-roadmap.md @@ -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 From 0ca9eb2c1cd2f7d7f042a9108db37e952028cf25 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 16 Jun 2026 18:44:36 -0700 Subject: [PATCH 04/14] fix(warp-core): require named plurality laws --- CHANGELOG.md | 3 ++- crates/warp-core/src/plurality_law.rs | 8 +++++++- .../tests/plurality_law_public_api_tests.rs | 12 ++++++++++++ .../goalpost-05-named-plurality-laws.md | 7 ++++--- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd6fe8e3..593d3fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,8 @@ 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. + obstruction evidence for unsupported or unauthorized law execution. Law + references reject all-zero names and zero versions before registration. - `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/plurality_law.rs b/crates/warp-core/src/plurality_law.rs index 55d45733..554bc1e4 100644 --- a/crates/warp-core/src/plurality_law.rs +++ b/crates/warp-core/src/plurality_law.rs @@ -103,6 +103,9 @@ impl PluralityLawFamily { /// 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, @@ -122,11 +125,14 @@ impl PluralityLawRef { /// # Errors /// /// Returns [`PluralityLawRefError::ZeroVersion`] when `version` is zero. - pub const fn new( + 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); } diff --git a/crates/warp-core/tests/plurality_law_public_api_tests.rs b/crates/warp-core/tests/plurality_law_public_api_tests.rs index 53df66b3..5f27616a 100644 --- a/crates/warp-core/tests/plurality_law_public_api_tests.rs +++ b/crates/warp-core/tests/plurality_law_public_api_tests.rs @@ -62,6 +62,18 @@ fn public_plurality_law_registry_registers_machine_readable_cards( 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::new(PluralityLawFamily::Settlement, law_name(0x55), 0), + Err(PluralityLawRefError::ZeroVersion) + ); +} + #[test] fn public_plurality_law_reading_identity_binds_law_name_and_version( ) -> Result<(), Box> { 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 6ce29a1e..79b86b55 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 @@ -61,9 +61,10 @@ PluralityLawFamily + version ``` -Law versions start at 1. Existing braid settlement policy ids map into -`PluralityLawRef::settlement_policy(...)`, preserving the current retained -policy identity while making the law family and version explicit in replay. +Law names cannot be the all-zero digest, and law versions start at 1. Existing +braid settlement policy ids map into `PluralityLawRef::settlement_policy(...)`, +preserving the current retained policy identity while making the law family and +version explicit in replay. `PluralityLawFamily` is core-generic: settlement, collapse, conflict-preserving, quorum, authority, and adapter-provided. Adapter-provided From 3390bb5ef549697edad83f25b458547ff1ae3719 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 02:43:49 -0700 Subject: [PATCH 05/14] fix(warp-core): prevent self-witness receipt overclaims --- crates/warp-core/src/braid_shell.rs | 10 +-- crates/warp-core/src/plurality_law.rs | 2 +- crates/warp-core/src/witness.rs | 88 +++++++++++++++---- .../tests/witness_public_api_tests.rs | 51 ++++++++--- 4 files changed, 118 insertions(+), 33 deletions(-) diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index 5f55d70e..b11c190d 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -1700,15 +1700,15 @@ 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.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, + audit.witness_receipt.compatibility(), WitnessCompatibilityRule::E1Scaffold ); assert_eq!( - audit.witness_receipt.attestation, + audit.witness_receipt.attestation(), WitnessAttestation::IntegrityOnly ); } diff --git a/crates/warp-core/src/plurality_law.rs b/crates/warp-core/src/plurality_law.rs index 554bc1e4..bd2e1bac 100644 --- a/crates/warp-core/src/plurality_law.rs +++ b/crates/warp-core/src/plurality_law.rs @@ -639,7 +639,7 @@ impl PluralityLawReading { } fn evidence_posture_for(receipt: WitnessReceipt) -> PluralityLawEvidencePosture { - match (receipt.kind, receipt.attestation) { + match (receipt.kind(), receipt.attestation()) { (WitnessKind::SelfWitness, WitnessAttestation::IntegrityOnly) => { PluralityLawEvidencePosture::SelfWitnessIntegrityOnly } diff --git a/crates/warp-core/src/witness.rs b/crates/warp-core/src/witness.rs index 185f648e..1dc30a14 100644 --- a/crates/warp-core/src/witness.rs +++ b/crates/warp-core/src/witness.rs @@ -105,34 +105,50 @@ impl WitnessCompatibilityRule { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WitnessReceipt { /// Witness family. - pub kind: WitnessKind, + kind: WitnessKind, /// Digest of the subject being witnessed. - pub subject_digest: Hash, + subject_digest: Hash, /// Digest of the witness evidence material. - pub evidence_digest: Hash, + evidence_digest: Hash, /// Compatibility rule governing receipt identity. - pub compatibility: WitnessCompatibilityRule, + compatibility: WitnessCompatibilityRule, /// Attestation strength claimed by this receipt. - pub attestation: WitnessAttestation, + attestation: WitnessAttestation, } impl WitnessReceipt { /// Creates a witness receipt with an explicit compatibility rule. - #[must_use] - pub const fn new( + /// + /// # 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, - ) -> Self { - Self { + ) -> 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. @@ -141,13 +157,43 @@ impl WitnessReceipt { /// independent attestation. #[must_use] pub const fn self_witness(subject_digest: Hash, evidence_digest: Hash) -> Self { - Self::new( - WitnessKind::SelfWitness, + Self { + kind: WitnessKind::SelfWitness, subject_digest, evidence_digest, - WitnessCompatibilityRule::E1Scaffold, - WitnessAttestation::IntegrityOnly, - ) + 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. @@ -230,6 +276,14 @@ pub enum WitnessError { /// 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 { @@ -310,7 +364,7 @@ impl WitnessBackend for WitnessBackendSimulator { request.evidence_digest, request.compatibility, WitnessAttestation::IndependentAttestation, - )) + )?) } WitnessSimulatorFixture::ThresholdWitnessFixture if request.kind == WitnessKind::ThresholdWitness => @@ -321,7 +375,7 @@ impl WitnessBackend for WitnessBackendSimulator { request.evidence_digest, request.compatibility, WitnessAttestation::IndependentAttestation, - )) + )?) } WitnessSimulatorFixture::RejectedWitnessFixture => Err(WitnessError::BackendRejected { kind: request.kind, diff --git a/crates/warp-core/tests/witness_public_api_tests.rs b/crates/warp-core/tests/witness_public_api_tests.rs index 66192c8d..3575bf61 100644 --- a/crates/warp-core/tests/witness_public_api_tests.rs +++ b/crates/warp-core/tests/witness_public_api_tests.rs @@ -57,12 +57,12 @@ fn public_witness_simulator_returns_deterministic_fixture_receipts() -> Result<( let second = backend.verify(&request)?; assert_eq!(first, second); - assert_eq!(first.kind, WitnessKind::SignedWitness); + assert_eq!(first.kind(), WitnessKind::SignedWitness); assert_eq!( - first.attestation, + first.attestation(), WitnessAttestation::IndependentAttestation ); - assert_eq!(first.compatibility, WitnessCompatibilityRule::StableV1); + assert_eq!(first.compatibility(), WitnessCompatibilityRule::StableV1); assert_eq!(first.digest(), second.digest()); Ok(()) } @@ -106,23 +106,54 @@ fn public_self_witness_fixture_rejects_stable_identity_requests() { } #[test] -fn public_witness_receipt_identity_binds_compatibility_rule() { +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::SelfWitness, + WitnessKind::SignedWitness, subject_digest(), evidence_digest(), WitnessCompatibilityRule::E1Scaffold, - WitnessAttestation::IntegrityOnly, - ); + WitnessAttestation::IndependentAttestation, + )?; let stable = WitnessReceipt::new( - WitnessKind::SelfWitness, + WitnessKind::SignedWitness, subject_digest(), evidence_digest(), WitnessCompatibilityRule::StableV1, - WitnessAttestation::IntegrityOnly, - ); + WitnessAttestation::IndependentAttestation, + )?; assert_ne!(scaffold.digest(), stable.digest()); + Ok(()) } #[test] From 57e47dcc5895b2766f3eafd40ad01f3d8728fbd5 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 02:46:06 -0700 Subject: [PATCH 06/14] fix(warp-core): derive law evidence from attestation --- CHANGELOG.md | 4 ++- crates/warp-core/src/plurality_law.rs | 12 ++++----- .../tests/plurality_law_public_api_tests.rs | 27 ++++++++++++++++++- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 593d3fee..666af780 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,9 @@ 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 reject all-zero names and zero versions before registration. + references reject all-zero names and zero versions 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. - `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/plurality_law.rs b/crates/warp-core/src/plurality_law.rs index bd2e1bac..89aa7cbf 100644 --- a/crates/warp-core/src/plurality_law.rs +++ b/crates/warp-core/src/plurality_law.rs @@ -10,7 +10,7 @@ use thiserror::Error; use crate::ident::Hash; use crate::revelation::AuthorityDomainRef; use crate::sealed_membership::DisclosureBudget; -use crate::witness::{WitnessAttestation, WitnessKind, WitnessReceipt}; +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"; @@ -271,7 +271,7 @@ impl PluralityLawConcealment { /// Evidence posture attached to plurality law execution. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum PluralityLawEvidencePosture { - /// E1 local self-witness integrity evidence only. + /// Integrity-only evidence, including E1 self-witness scaffolding. SelfWitnessIntegrityOnly, /// Independent external witness evidence. ExternalWitness, @@ -639,11 +639,9 @@ impl PluralityLawReading { } fn evidence_posture_for(receipt: WitnessReceipt) -> PluralityLawEvidencePosture { - match (receipt.kind(), receipt.attestation()) { - (WitnessKind::SelfWitness, WitnessAttestation::IntegrityOnly) => { - PluralityLawEvidencePosture::SelfWitnessIntegrityOnly - } - _ => PluralityLawEvidencePosture::ExternalWitness, + match receipt.attestation() { + WitnessAttestation::IntegrityOnly => PluralityLawEvidencePosture::SelfWitnessIntegrityOnly, + WitnessAttestation::IndependentAttestation => PluralityLawEvidencePosture::ExternalWitness, } } diff --git a/crates/warp-core/tests/plurality_law_public_api_tests.rs b/crates/warp-core/tests/plurality_law_public_api_tests.rs index 5f27616a..157f32c1 100644 --- a/crates/warp-core/tests/plurality_law_public_api_tests.rs +++ b/crates/warp-core/tests/plurality_law_public_api_tests.rs @@ -8,7 +8,8 @@ use warp_core::{ PluralityLawCardError, PluralityLawConcealment, PluralityLawEmission, PluralityLawEvidencePosture, PluralityLawFamily, PluralityLawName, PluralityLawObstruction, PluralityLawObstructionKind, PluralityLawReading, PluralityLawRef, PluralityLawRefError, - PluralityLawRegistry, PluralityLawRegistryError, PluralityLawRequirement, WitnessReceipt, + PluralityLawRegistry, PluralityLawRegistryError, PluralityLawRequirement, WitnessAttestation, + WitnessCompatibilityRule, WitnessKind, WitnessReceipt, }; fn law_name(byte: u8) -> PluralityLawName { @@ -97,6 +98,30 @@ fn public_plurality_law_reading_identity_binds_law_name_and_version( Ok(()) } +#[test] +fn public_plurality_law_reading_does_not_promote_integrity_only_receipts( +) -> Result<(), Box> { + let witness = WitnessReceipt::new( + WitnessKind::SignedWitness, + [0xAA; 32], + [0xBB; 32], + WitnessCompatibilityRule::StableV1, + WitnessAttestation::IntegrityOnly, + )?; + let reading = PluralityLawReading::new( + law_ref(0x56, 1)?, + [0xCC; 32], + witness, + DisclosureBudget::AuthorityScoped, + ); + + assert_eq!( + reading.evidence_posture(), + PluralityLawEvidencePosture::SelfWitnessIntegrityOnly + ); + Ok(()) +} + #[test] fn public_adapter_provided_laws_route_through_authority_without_app_nouns( ) -> Result<(), Box> { From 7c9958649f17bc403e9c22d9bdd0c78a0c795e3d Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 02:49:38 -0700 Subject: [PATCH 07/14] fix(warp-core): bind sealed presentations to receipts --- CHANGELOG.md | 5 +- crates/warp-core/src/lib.rs | 5 +- crates/warp-core/src/sealed_membership.rs | 185 ++++++++++++++++-- .../tests/witness_public_api_tests.rs | 85 ++++++-- ...itness-receipts-and-sealed-capabilities.md | 4 +- 5 files changed, 254 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666af780..ab3fdc09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,10 @@ 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. + 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, diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 8b38d0c2..339289bf 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -264,7 +264,10 @@ pub use retained_evidence::{ RetainedEvidenceCoordinate, RetainedEvidencePosture, RetainedEvidenceRef, RetainedEvidenceRole, }; // --- Sealed membership capability types --- -pub use sealed_membership::{DisclosureBudget, PresentationPurpose, SealedMembershipPresentation}; +pub use sealed_membership::{ + DisclosureBudget, PresentationPurpose, SealedMembershipPresentation, + SealedMembershipPresentationError, +}; // --- Session types --- pub use playback::{SessionId, ViewSession}; // --- Proof types --- diff --git a/crates/warp-core/src/sealed_membership.rs b/crates/warp-core/src/sealed_membership.rs index 01498273..a01fc763 100644 --- a/crates/warp-core/src/sealed_membership.rs +++ b/crates/warp-core/src/sealed_membership.rs @@ -2,11 +2,17 @@ // © 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 { @@ -45,6 +51,27 @@ impl PresentationPurpose { } } +/// 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, @@ -53,37 +80,173 @@ impl PresentationPurpose { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SealedMembershipPresentation { /// Braid coordinate the membership claim is scoped to. - pub braid_coordinate: BraidCoordinate, + braid_coordinate: BraidCoordinate, /// Generic capability purpose for the presentation. - pub purpose: PresentationPurpose, + purpose: PresentationPurpose, /// Authority domain under which the sealed member commitment is meaningful. - pub authority_domain: AuthorityDomainRef, + authority_domain: AuthorityDomainRef, /// Blinded member commitment presented for the purpose. - pub member_commitment: Hash, + member_commitment: Hash, /// Witness receipt supporting this presentation. - pub witness_receipt: WitnessReceipt, + witness_receipt: WitnessReceipt, /// Disclosure budget governing the presentation. - pub disclosure_budget: DisclosureBudget, + disclosure_budget: DisclosureBudget, } impl SealedMembershipPresentation { /// Creates a purpose-bound sealed membership presentation. - #[must_use] - pub const fn new( + /// + /// # 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, - ) -> Self { - Self { + ) -> Result { + let expected_subject = Self::witness_subject_digest( braid_coordinate, purpose, authority_domain, member_commitment, - witness_receipt, 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/tests/witness_public_api_tests.rs b/crates/warp-core/tests/witness_public_api_tests.rs index 3575bf61..e362b7bd 100644 --- a/crates/warp-core/tests/witness_public_api_tests.rs +++ b/crates/warp-core/tests/witness_public_api_tests.rs @@ -5,9 +5,10 @@ use warp_core::{ AuthorityDomainId, AuthorityDomainRef, BraidCoordinate, DisclosureBudget, OriginId, - PresentationPurpose, SealedMembershipPresentation, WitnessAttestation, WitnessBackend, - WitnessBackendSimulator, WitnessCompatibilityRule, WitnessError, WitnessKind, WitnessReceipt, - WitnessRejectionCode, WitnessRequest, WitnessSimulatorFixture, + PresentationPurpose, SealedMembershipPresentation, SealedMembershipPresentationError, + WitnessAttestation, WitnessBackend, WitnessBackendSimulator, WitnessCompatibilityRule, + WitnessError, WitnessKind, WitnessReceipt, WitnessRejectionCode, WitnessRequest, + WitnessSimulatorFixture, }; fn subject_digest() -> [u8; 32] { @@ -157,23 +158,75 @@ fn public_witness_receipt_identity_binds_compatibility_rule() -> Result<(), Witn } #[test] -fn public_sealed_membership_presentation_uses_generic_purpose_and_budget() { +fn public_sealed_membership_presentation_rejects_unbound_receipts() { let purpose = PresentationPurpose::new([0x44; 32]); - let receipt = WitnessReceipt::self_witness(subject_digest(), evidence_digest()); - let presentation = SealedMembershipPresentation::new( - BraidCoordinate([0xBC; 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_ref(), - [0xA5; 32], - receipt, - DisclosureBudget::CapabilityScoped, + authority, + member_commitment, + disclosure_budget, ); + let receipt = WitnessReceipt::self_witness([0xDE; 32], evidence_digest()); - assert_eq!(presentation.purpose, purpose); - assert_eq!(presentation.purpose.purpose_id(), [0x44; 32]); assert_eq!( - presentation.disclosure_budget, - DisclosureBudget::CapabilityScoped + SealedMembershipPresentation::new( + coordinate, + purpose, + authority, + member_commitment, + receipt, + disclosure_budget, + ), + Err(SealedMembershipPresentationError::WitnessSubjectMismatch { + expected, + actual: [0xDE; 32], + }) + ); +} + +#[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, ); - assert_eq!(presentation.witness_receipt, receipt); + 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 f94e0640..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 @@ -89,7 +89,9 @@ 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`. +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`; From 89ec42dc80d209f6c50522e609e82bd74583aaef Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 02:53:57 -0700 Subject: [PATCH 08/14] fix(warp-core): reject empty policy law refs --- CHANGELOG.md | 7 +-- crates/warp-core/src/braid_shell.rs | 47 ++++++++++++++++--- crates/warp-core/src/plurality_law.rs | 18 ++++--- .../tests/plurality_law_public_api_tests.rs | 4 ++ .../goalpost-05-named-plurality-laws.md | 8 ++-- 5 files changed, 64 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab3fdc09..93cdd9d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,9 +55,10 @@ 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 reject all-zero names and zero versions 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. + references and braid shell policy ids reject all-zero names before replay, 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. - `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 b11c190d..e164e094 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -375,6 +375,9 @@ pub enum BraidShellError { /// 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 lineage parent shell is missing or not plural. #[error("lineage parent {parent:?} is missing or not plural")] InvalidLineageParent { @@ -474,7 +477,8 @@ 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 @@ -523,6 +527,7 @@ impl BraidShell { if members.is_empty() { return Err(BraidShellError::EmptyMembers); } + check_policy_id(policy_id)?; members.sort_by_cached_key(BraidShellMember::member_digest); check_unique_member_strands(&members)?; if let BraidShellOutcome::Plural { alternative_ids } = &mut outcome { @@ -603,8 +608,8 @@ 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 + /// non-canonical or duplicate plural alternatives, an empty policy id, 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. pub fn validate(&self) -> Result<(), BraidShellError> { @@ -617,6 +622,7 @@ impl BraidShell { if self.members.is_empty() { return Err(BraidShellError::EmptyMembers); } + check_policy_id(self.policy_id)?; // Compute each member digest once; the order check, coordinate, // witness, and shell digests all consume it. let member_digests: Vec = self @@ -749,6 +755,13 @@ 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(()) +} + /// 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() { @@ -1032,7 +1045,7 @@ pub fn replay_braid_shell( records: &dyn BraidShellRecords, ) -> Result { let shell = validated_shell_for_replay(digest, records)?; - let law_ref = PluralityLawRef::settlement_policy(shell.policy_id); + let law_ref = settlement_law_ref(shell.policy_id)?; Ok(BraidShellReplay { outcome_kind: shell.outcome_kind(), member_verdicts: shell @@ -1080,7 +1093,7 @@ pub fn audit_braid_shell( } }); let witness_receipt = WitnessReceipt::self_witness(shell.digest, shell.witness_digest); - let law_ref = PluralityLawRef::settlement_policy(shell.policy_id); + let law_ref = settlement_law_ref(shell.policy_id)?; let law_reading = PluralityLawReading::new( law_ref, shell.digest, @@ -1144,6 +1157,10 @@ fn shell_disclosure_budget(shell: &BraidShell) -> DisclosureBudget { } } +fn settlement_law_ref(policy_id: Hash) -> Result { + PluralityLawRef::settlement_policy(policy_id).map_err(|_| BraidShellError::EmptyPolicyId) +} + fn validated_shell_for_replay<'a>( digest: &Hash, records: &'a dyn BraidShellRecords, @@ -1603,7 +1620,7 @@ mod tests { assert_eq!(replay.member_verdicts, expected_verdicts); assert_eq!(replay.policy_id, [0x5E; 32]); assert_eq!( - replay.law_ref, + Ok(replay.law_ref), PluralityLawRef::settlement_policy([0x5E; 32]) ); assert_eq!(replay.posture, CausalPosture::AuthorOnly); @@ -1656,7 +1673,7 @@ mod tests { assert_eq!(audit.shell_digest, digest); assert_eq!(audit.outcome_kind, AdmissionOutcomeKind::Plural); assert_eq!( - audit.law_reading.law_ref(), + Ok(audit.law_reading.law_ref()), PluralityLawRef::settlement_policy([0x5E; 32]) ); assert_eq!(audit.law_reading.support_digest(), digest); @@ -1816,6 +1833,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( diff --git a/crates/warp-core/src/plurality_law.rs b/crates/warp-core/src/plurality_law.rs index 89aa7cbf..597aa148 100644 --- a/crates/warp-core/src/plurality_law.rs +++ b/crates/warp-core/src/plurality_law.rs @@ -144,13 +144,17 @@ impl PluralityLawRef { } /// Constructs the v1 settlement-law reference for an existing policy id. - #[must_use] - pub const fn settlement_policy(policy_id: Hash) -> Self { - Self { - family: PluralityLawFamily::Settlement, - name: PluralityLawName::from_bytes(policy_id), - version: 1, - } + /// + /// # 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, + ) } /// Returns the law family. diff --git a/crates/warp-core/tests/plurality_law_public_api_tests.rs b/crates/warp-core/tests/plurality_law_public_api_tests.rs index 157f32c1..97e228a0 100644 --- a/crates/warp-core/tests/plurality_law_public_api_tests.rs +++ b/crates/warp-core/tests/plurality_law_public_api_tests.rs @@ -69,6 +69,10 @@ fn public_plurality_law_ref_requires_name_and_version() { PluralityLawRef::new(PluralityLawFamily::Settlement, law_name(0x00), 1), Err(PluralityLawRefError::EmptyName) ); + assert_eq!( + PluralityLawRef::settlement_policy([0; 32]), + Err(PluralityLawRefError::EmptyName) + ); assert_eq!( PluralityLawRef::new(PluralityLawFamily::Settlement, law_name(0x55), 0), Err(PluralityLawRefError::ZeroVersion) 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 79b86b55..0a53c1fc 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 @@ -62,9 +62,11 @@ PluralityLawFamily ``` Law names cannot be the all-zero digest, and law versions start at 1. Existing -braid settlement policy ids map into `PluralityLawRef::settlement_policy(...)`, -preserving the current retained policy identity while making the law family and -version explicit in replay. +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. `PluralityLawFamily` is core-generic: settlement, collapse, conflict-preserving, quorum, authority, and adapter-provided. Adapter-provided From d67b25cdfa6e144ae6a2b623623d5efda02b144c Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 02:56:36 -0700 Subject: [PATCH 09/14] fix(warp-core): report collapse policies as laws --- CHANGELOG.md | 9 +++--- crates/warp-core/src/braid_shell.rs | 29 ++++++++++++++----- crates/warp-core/src/plurality_law.rs | 14 +++++++++ .../tests/plurality_law_public_api_tests.rs | 8 +++++ .../goalpost-05-named-plurality-laws.md | 5 +++- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93cdd9d8..83b86eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,10 +55,11 @@ 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, 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. + 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. - `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 e164e094..2c688016 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -936,9 +936,9 @@ 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 settlement law that interpreted retained plurality. + /// Named law that interpreted retained plurality or collapse. pub law_ref: PluralityLawRef, /// Witness digest binding the act. pub witness_digest: Hash, @@ -1008,7 +1008,7 @@ 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, @@ -1045,7 +1045,7 @@ pub fn replay_braid_shell( records: &dyn BraidShellRecords, ) -> Result { let shell = validated_shell_for_replay(digest, records)?; - let law_ref = settlement_law_ref(shell.policy_id)?; + let law_ref = shell_law_ref(shell)?; Ok(BraidShellReplay { outcome_kind: shell.outcome_kind(), member_verdicts: shell @@ -1093,7 +1093,7 @@ pub fn audit_braid_shell( } }); let witness_receipt = WitnessReceipt::self_witness(shell.digest, shell.witness_digest); - let law_ref = settlement_law_ref(shell.policy_id)?; + let law_ref = shell_law_ref(shell)?; let law_reading = PluralityLawReading::new( law_ref, shell.digest, @@ -1157,8 +1157,15 @@ fn shell_disclosure_budget(shell: &BraidShell) -> DisclosureBudget { } } -fn settlement_law_ref(policy_id: Hash) -> Result { - PluralityLawRef::settlement_policy(policy_id).map_err(|_| BraidShellError::EmptyPolicyId) +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>( @@ -2117,6 +2124,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(); @@ -2147,6 +2156,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/plurality_law.rs b/crates/warp-core/src/plurality_law.rs index 597aa148..4cb40c7f 100644 --- a/crates/warp-core/src/plurality_law.rs +++ b/crates/warp-core/src/plurality_law.rs @@ -157,6 +157,20 @@ impl PluralityLawRef { ) } + /// 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 { diff --git a/crates/warp-core/tests/plurality_law_public_api_tests.rs b/crates/warp-core/tests/plurality_law_public_api_tests.rs index 97e228a0..64f91f8c 100644 --- a/crates/warp-core/tests/plurality_law_public_api_tests.rs +++ b/crates/warp-core/tests/plurality_law_public_api_tests.rs @@ -73,6 +73,14 @@ fn public_plurality_law_ref_requires_name_and_version() { 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) 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 0a53c1fc..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 @@ -66,7 +66,10 @@ 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. +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 From 116e491a0a48190139cbe1413500d0e388861044 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 02:59:10 -0700 Subject: [PATCH 10/14] test(warp-core): assert sealed shell disclosure budget --- crates/warp-core/src/braid_shell.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index 2c688016..100e76ac 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -1761,6 +1761,10 @@ mod tests { audit.member_facts[0].disclosure_budget, DisclosureBudget::AuthorityScoped ); + assert_eq!( + audit.law_reading.disclosure_budget(), + DisclosureBudget::AuthorityScoped + ); } #[test] From aa04326cb919af45e76782a0b8d9954b42cbe385 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 03:00:21 -0700 Subject: [PATCH 11/14] perf(warp-core): avoid law card digest allocation --- crates/warp-core/src/plurality_law.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/warp-core/src/plurality_law.rs b/crates/warp-core/src/plurality_law.rs index 4cb40c7f..a6c7e12b 100644 --- a/crates/warp-core/src/plurality_law.rs +++ b/crates/warp-core/src/plurality_law.rs @@ -673,10 +673,11 @@ 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 Iterator) { - let tags: Vec = tags.collect(); +fn hash_tag_vec(hasher: &mut Hasher, tags: impl ExactSizeIterator) { hash_len(hasher, tags.len()); - hasher.update(&tags); + for tag in tags { + hasher.update(&[tag]); + } } fn authority_digest(authority: AuthorityDomainRef) -> Hash { From bd18b984cbcf155d064ede25afd2a9aa13084d85 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 03:06:11 -0700 Subject: [PATCH 12/14] fix(warp-core): bind law readings to support receipts --- CHANGELOG.md | 3 +- crates/warp-core/src/braid_shell.rs | 7 +++- crates/warp-core/src/lib.rs | 5 ++- crates/warp-core/src/plurality_law.rs | 34 +++++++++++++-- .../tests/plurality_law_public_api_tests.rs | 42 ++++++++++++++----- 5 files changed, 72 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b86eec..1a61d538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,8 @@ 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. + are not promoted to external witness evidence, and they reject witness + receipts whose subject digest does not match the retained support digest. - `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 100e76ac..41adf3fc 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -21,7 +21,7 @@ use blake3::Hasher; use crate::admission::AdmissionOutcomeKind; use crate::ident::Hash; -use crate::plurality_law::{PluralityLawReading, PluralityLawRef}; +use crate::plurality_law::{PluralityLawReading, PluralityLawReadingError, PluralityLawRef}; use crate::provenance_store::ProvenanceRef; use crate::revelation::{ shell_posture_obstruction, AuthorityDomainRef, CausalPosture, PostureObstruction, WitnessDigest, @@ -378,6 +378,9 @@ pub enum BraidShellError { /// 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 { @@ -1099,7 +1102,7 @@ pub fn audit_braid_shell( shell.digest, witness_receipt, shell_disclosure_budget(shell), - ); + )?; Ok(BraidShellAudit { shell_digest: shell.digest, diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 339289bf..a89c73fb 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -250,8 +250,9 @@ pub use payload::{ pub use plurality_law::{ PluralityLawAuthorization, PluralityLawCard, PluralityLawCardError, PluralityLawConcealment, PluralityLawEmission, PluralityLawEvidencePosture, PluralityLawFamily, PluralityLawName, - PluralityLawObstruction, PluralityLawObstructionKind, PluralityLawReading, PluralityLawRef, - PluralityLawRefError, PluralityLawRegistry, PluralityLawRegistryError, PluralityLawRequirement, + PluralityLawObstruction, PluralityLawObstructionKind, PluralityLawReading, + PluralityLawReadingError, PluralityLawRef, PluralityLawRefError, PluralityLawRegistry, + PluralityLawRegistryError, PluralityLawRequirement, }; // --- Cursor types --- pub use contract_obstruction::{ diff --git a/crates/warp-core/src/plurality_law.rs b/crates/warp-core/src/plurality_law.rs index a6c7e12b..1ab3cc4d 100644 --- a/crates/warp-core/src/plurality_law.rs +++ b/crates/warp-core/src/plurality_law.rs @@ -593,23 +593,49 @@ pub struct PluralityLawReading { 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. - #[must_use] + /// + /// # 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, - ) -> Self { + ) -> 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); - Self { + Ok(Self { law_ref, support_digest, witness_receipt, evidence_posture, disclosure_budget, - } + }) } /// Returns the law reference used for this reading. diff --git a/crates/warp-core/tests/plurality_law_public_api_tests.rs b/crates/warp-core/tests/plurality_law_public_api_tests.rs index 64f91f8c..06383bcf 100644 --- a/crates/warp-core/tests/plurality_law_public_api_tests.rs +++ b/crates/warp-core/tests/plurality_law_public_api_tests.rs @@ -7,9 +7,9 @@ use warp_core::{ AuthorityDomainId, AuthorityDomainRef, DisclosureBudget, OriginId, PluralityLawCard, PluralityLawCardError, PluralityLawConcealment, PluralityLawEmission, PluralityLawEvidencePosture, PluralityLawFamily, PluralityLawName, PluralityLawObstruction, - PluralityLawObstructionKind, PluralityLawReading, PluralityLawRef, PluralityLawRefError, - PluralityLawRegistry, PluralityLawRegistryError, PluralityLawRequirement, WitnessAttestation, - WitnessCompatibilityRule, WitnessKind, WitnessReceipt, + PluralityLawObstructionKind, PluralityLawReading, PluralityLawReadingError, PluralityLawRef, + PluralityLawRefError, PluralityLawRegistry, PluralityLawRegistryError, PluralityLawRequirement, + WitnessAttestation, WitnessCompatibilityRule, WitnessKind, WitnessReceipt, }; fn law_name(byte: u8) -> PluralityLawName { @@ -93,16 +93,16 @@ fn public_plurality_law_reading_identity_binds_law_name_and_version( let witness = WitnessReceipt::self_witness([0xAA; 32], [0xBB; 32]); let v1 = PluralityLawReading::new( law_ref(0x52, 1)?, - [0xCC; 32], + witness.subject_digest(), witness, DisclosureBudget::AuthorityScoped, - ); + )?; let v2 = PluralityLawReading::new( law_ref(0x52, 2)?, - [0xCC; 32], + witness.subject_digest(), witness, DisclosureBudget::AuthorityScoped, - ); + )?; assert_eq!(v1.law_ref().version(), 1); assert_eq!(v2.law_ref().version(), 2); @@ -113,19 +113,20 @@ fn public_plurality_law_reading_identity_binds_law_name_and_version( #[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, - [0xAA; 32], + support_digest, [0xBB; 32], WitnessCompatibilityRule::StableV1, WitnessAttestation::IntegrityOnly, )?; let reading = PluralityLawReading::new( law_ref(0x56, 1)?, - [0xCC; 32], + support_digest, witness, DisclosureBudget::AuthorityScoped, - ); + )?; assert_eq!( reading.evidence_posture(), @@ -134,6 +135,27 @@ fn public_plurality_law_reading_does_not_promote_integrity_only_receipts( 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> { From 9da73d6c9596c3f74add555aba8af978b5af3591 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 04:20:57 -0700 Subject: [PATCH 13/14] fix(warp-core): require collapse policy identity coherence --- CHANGELOG.md | 2 + crates/warp-core/src/braid_shell.rs | 71 ++++++++++++++++++++++++--- crates/warp-core/src/plurality_law.rs | 3 ++ 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a61d538..1ce1c5d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,8 @@ 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 41adf3fc..dba88b8b 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -372,6 +372,14 @@ 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, @@ -484,8 +492,9 @@ impl BraidShell { /// ([`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`]). @@ -531,6 +540,7 @@ impl BraidShell { 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 { @@ -612,9 +622,10 @@ 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 policy id, 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. + /// 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 { @@ -626,6 +637,7 @@ impl BraidShell { 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 @@ -765,6 +777,25 @@ fn check_policy_id(policy_id: Hash) -> Result<(), BraidShellError> { 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() { @@ -1818,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]), @@ -2049,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]), @@ -2074,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)]); diff --git a/crates/warp-core/src/plurality_law.rs b/crates/warp-core/src/plurality_law.rs index 1ab3cc4d..94f961ee 100644 --- a/crates/warp-core/src/plurality_law.rs +++ b/crates/warp-core/src/plurality_law.rs @@ -124,6 +124,9 @@ impl PluralityLawRef { /// /// # Errors /// + /// Returns [`PluralityLawRefError::EmptyName`] when `name` is the all-zero + /// digest. + /// /// Returns [`PluralityLawRefError::ZeroVersion`] when `version` is zero. pub fn new( family: PluralityLawFamily, From ba972f6f64c72351d89eb1c7b7c3c480e538feb1 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 17 Jun 2026 04:48:38 -0700 Subject: [PATCH 14/14] test(warp-core): cover sealed presentation evidence mismatch --- .../tests/witness_public_api_tests.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/warp-core/tests/witness_public_api_tests.rs b/crates/warp-core/tests/witness_public_api_tests.rs index e362b7bd..d022daa9 100644 --- a/crates/warp-core/tests/witness_public_api_tests.rs +++ b/crates/warp-core/tests/witness_public_api_tests.rs @@ -189,6 +189,47 @@ fn public_sealed_membership_presentation_rejects_unbound_receipts() { ); } +#[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> {