Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

### Added

- `warp-core` now hardens the first braids/strands roadmap goalpost: `Strand<P>`
fields are no longer publicly constructible, `Strand::new(...)` validates
typestate/runtime-posture coherence before construction, public strand tests
use fixture builders and accessors, `ProofEnvelope::validate_shape(...)`
returns structured `ProofError`, and invalid braid lifecycle transitions
report typed `BraidTransitionKind` instead of action strings.
- `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
boundary used by external callers.
- `warp-core` casting a dynamically postured strand to statically shared now returns a semantically precise `PostureObstruction::PostureMismatch` instead of `PostureObstruction::NarrowingRefused`.
- `warp-core` renamed `ProofEnvelope::verify` to `validate_shape` and updated error variants to `ProofShapeValidationFailed` to accurately reflect shape/input checks rather than full cryptographic proof verification.
- `warp-core` strand creation now carries explicit `RetentionPosture` through
Expand Down
10 changes: 6 additions & 4 deletions crates/echo-wesley-gen/tests/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! Integration test for the echo-wesley-gen CLI (Wesley IR -> Rust code).

use std::fs;
use std::io::Write;
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::OnceLock;
Expand Down Expand Up @@ -131,9 +131,11 @@ fn run_wesley_gen_with_args(ir: &str, args: &[&str]) -> Output {
.expect("failed to spawn echo-wesley-gen");

let mut stdin = child.stdin.take().expect("failed to get stdin");
stdin
.write_all(ir.as_bytes())
.expect("failed to write to stdin");
match stdin.write_all(ir.as_bytes()) {
Ok(()) => {}
Err(err) if err.kind() == ErrorKind::BrokenPipe => {}
Err(err) => panic!("failed to write to stdin: {err}"),
}
drop(stdin);

child.wait_with_output().expect("failed to wait on child")
Expand Down
43 changes: 34 additions & 9 deletions crates/warp-core/src/braid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,31 @@ pub enum BraidStatus {
Collapsed,
}

/// Typed lifecycle transition attempted against a braid.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BraidTransitionKind {
/// Create the braid event log.
Create,
/// Weave a member into the active frontier.
WeaveMember,
/// Finalize settlement for the current frontier.
FinalizeSettlement,
/// Collapse a finalized plural braid.
Collapse,
}

impl std::fmt::Display for BraidTransitionKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = match self {
Self::Create => "create braid",
Self::WeaveMember => "weave member",
Self::FinalizeSettlement => "finalize settlement",
Self::Collapse => "collapse braid",
};
f.write_str(label)
}
}

/// Error kinds returned during coordination braid lifecycle updates or folds.
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum BraidError {
Expand All @@ -41,10 +66,10 @@ pub enum BraidError {
actual: u64,
},
/// An invalid transition was attempted for the current braid status.
#[error("cannot transition braid state: cannot {action} in status {status:?}")]
#[error("cannot transition braid state: cannot {transition} in status {status:?}")]
InvalidTransition {
/// Attempted action or event kind.
action: String,
/// Attempted lifecycle transition.
transition: BraidTransitionKind,
/// Current braid status.
status: BraidStatus,
},
Expand Down Expand Up @@ -161,7 +186,7 @@ impl Braid {
} => {
if self.status != BraidStatus::Active {
return Err(BraidError::InvalidTransition {
action: "weave member".to_string(),
transition: BraidTransitionKind::WeaveMember,
status: self.status,
});
}
Expand Down Expand Up @@ -195,7 +220,7 @@ impl Braid {
BraidEvent::SettlementFinalized { settlement_digest } => {
if self.status != BraidStatus::Active {
return Err(BraidError::InvalidTransition {
action: "finalize settlement".to_string(),
transition: BraidTransitionKind::FinalizeSettlement,
status: self.status,
});
}
Expand All @@ -211,7 +236,7 @@ impl Braid {
} => {
if self.status != BraidStatus::Finalized {
return Err(BraidError::InvalidTransition {
action: "collapse braid".to_string(),
transition: BraidTransitionKind::Collapse,
status: self.status,
});
}
Expand Down Expand Up @@ -459,7 +484,7 @@ mod tests {
assert_eq!(
Braid::fold(bad_events_weave_after_finalized),
Err(BraidError::InvalidTransition {
action: "weave member".to_string(),
transition: BraidTransitionKind::WeaveMember,
status: BraidStatus::Finalized
})
);
Expand All @@ -478,7 +503,7 @@ mod tests {
assert_eq!(
Braid::fold(bad_events_collapse_before_finalized),
Err(BraidError::InvalidTransition {
action: "collapse braid".to_string(),
transition: BraidTransitionKind::Collapse,
status: BraidStatus::Active
})
);
Expand Down Expand Up @@ -539,7 +564,7 @@ mod tests {
sequence_num: 2,
}),
Err(BraidError::InvalidTransition {
action: "weave member".to_string(),
transition: BraidTransitionKind::WeaveMember,
status: BraidStatus::Finalized,
})
);
Expand Down
21 changes: 15 additions & 6 deletions crates/warp-core/src/braid_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ pub enum BraidShellError {
#[error("proof shape validation failed: {reason}")]
ProofShapeValidationFailed {
/// Reason for shape validation failure.
reason: String,
reason: crate::proof::ProofError,
},
/// Member entries are not in canonical order.
#[error("braid shell members are not in canonical order")]
Expand Down Expand Up @@ -1891,7 +1891,7 @@ mod tests {

#[test]
fn assemble_with_proof_validates_envelope() {
use crate::proof::{ProofEnvelope, ProofKind};
use crate::proof::{ProofEnvelope, ProofError, ProofKind};

let members = vec![member("member-a", MemberVerdict::Plural)];

Expand Down Expand Up @@ -1966,7 +1966,12 @@ mod tests {
);
assert!(matches!(
result_mismatch,
Err(BraidShellError::ProofShapeValidationFailed { .. })
Err(BraidShellError::ProofShapeValidationFailed {
reason: ProofError::PublicInputsMismatch {
expected,
actual,
},
}) if expected == expected_witness && actual == [0x99; 32]
));

// Invalid proof: empty proof bytes
Expand All @@ -1988,13 +1993,15 @@ mod tests {
);
assert!(matches!(
result_empty,
Err(BraidShellError::ProofShapeValidationFailed { .. })
Err(BraidShellError::ProofShapeValidationFailed {
reason: ProofError::EmptyPayload,
})
));
}

#[test]
fn cryptographic_proof_kinds_require_verifier_backend() {
use crate::proof::{ProofEnvelope, ProofKind};
use crate::proof::{ProofEnvelope, ProofError, ProofKind};

let members = vec![member("member-a", MemberVerdict::Plural)];
let temp_shell = BraidShell::assemble(
Expand Down Expand Up @@ -2027,7 +2034,9 @@ mod tests {
);
assert!(matches!(
result,
Err(BraidShellError::ProofShapeValidationFailed { .. })
Err(BraidShellError::ProofShapeValidationFailed {
reason: ProofError::UnsupportedKind { kind: rejected },
}) if rejected == kind
));
}
}
Expand Down
73 changes: 64 additions & 9 deletions crates/warp-core/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1602,20 +1602,20 @@ impl WorldlineRuntime {
.map(|head| *head.key())
.collect::<Vec<_>>();
let retention_posture = request.retention_posture;
let strand = Strand::new(
request.strand_id,
fork_basis_ref,
request.child_worldline_id,
writer_heads.clone(),
Vec::new(),
retention_posture,
)?;

self.register_worldline(request.child_worldline_id, child_state)?;
for head in request.writer_heads {
self.register_writer_head(head)?;
}
self.register_strand(Strand {
strand_id: request.strand_id,
fork_basis_ref,
child_worldline_id: request.child_worldline_id,
writer_heads: writer_heads.clone(),
support_pins: Vec::new(),
retention_posture,
_marker: std::marker::PhantomData,
})?;
self.register_strand(strand)?;

Ok(ForkStrandReceipt {
strand_id: request.strand_id,
Expand Down Expand Up @@ -4068,6 +4068,61 @@ mod tests {
assert_eq!(provenance.len(child_worldline_id).unwrap(), 1);
}

#[test]
fn fork_strand_rejects_empty_writer_heads_and_rolls_back() {
let mut runtime = WorldlineRuntime::new();
let mut engine = empty_engine();
let source_lane_id = wl(1);
let child_worldline_id = wl(2);
let strand_id = make_strand_id("fork-empty-heads");

runtime
.register_worldline(source_lane_id, WorldlineState::empty())
.unwrap();
register_head(
&mut runtime,
source_lane_id,
"source-default",
None,
true,
InboxPolicy::AcceptAll,
);

let mut provenance = mirrored_provenance(&runtime);
commit_one_tick(
&mut runtime,
&mut provenance,
&mut engine,
source_lane_id,
"fork-empty-heads-source",
);

let err = runtime
.fork_strand(
&mut provenance,
ForkStrandRequest {
strand_id,
source_lane_id,
fork_tick: wt(0),
child_worldline_id,
writer_heads: Vec::new(),
retention_posture: test_retention_posture(12),
},
)
.unwrap_err();

assert!(matches!(
err,
RuntimeError::Strand(StrandError::InvariantViolation(
"INV-S2: v1 strands must carry exactly one writer head"
))
));
assert!(runtime.worldlines().get(&child_worldline_id).is_none());
assert!(runtime.strands().get(&strand_id).is_none());
assert!(provenance.len(child_worldline_id).is_err());
assert_eq!(provenance.len(source_lane_id).unwrap(), 1);
}

#[test]
fn fork_strand_rolls_back_runtime_and_provenance_on_error() {
let mut runtime = WorldlineRuntime::new();
Expand Down
6 changes: 4 additions & 2 deletions crates/warp-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,11 @@ pub use retained_evidence::{
// --- Session types ---
pub use playback::{SessionId, ViewSession};
// --- Proof types ---
pub use proof::{ObserverHonestyClaim, ProofEnvelope, ProofKind};
pub use proof::{
ObserverHonestyClaim, ProofEnvelope, ProofError, ProofKind, VerificationFailureCode,
};
// --- Braid Log types ---
pub use braid::{Braid, BraidError, BraidEvent, BraidStatus};
pub use braid::{Braid, BraidError, BraidEvent, BraidStatus, BraidTransitionKind};
// --- Retained boundary shell family (θ_tick, θ_braid) ---
pub use braid_shell::{
collapse_braid_shell, replay_braid_shell, BraidCoordinate, BraidMemberRef, BraidShell,
Expand Down
61 changes: 50 additions & 11 deletions crates/warp-core/src/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! Proof envelopes and honesty assertions.

use blake3::Hasher;
use thiserror::Error;

use crate::braid_shell::BraidCoordinate;
use crate::ident::Hash;
Expand Down Expand Up @@ -41,6 +42,46 @@ impl ProofKind {
}
}

/// Future verifier backend rejection code.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum VerificationFailureCode {
/// A verifier backend rejected the proof without a narrower public reason.
Rejected,
}

/// Structured proof envelope validation failure.
#[derive(Error, Clone, Debug, PartialEq, Eq)]
pub enum ProofError {
/// This proof kind is reserved until a verifier backend is wired.
#[error("{kind:?} proof envelopes require a verifier backend before admission")]
UnsupportedKind {
/// Unsupported proof kind.
kind: ProofKind,
},
/// The envelope carried no proof/evidence bytes.
#[error("proof payload is empty")]
EmptyPayload,
/// The envelope public inputs did not bind to the expected digest.
#[error("public inputs mismatch: expected {expected:?}, got {actual:?}")]
PublicInputsMismatch {
/// Expected public-input hash.
expected: Hash,
/// Actual public-input hash carried by the envelope.
actual: Hash,
},
/// The envelope shape was malformed before backend verification.
#[error("malformed proof envelope")]
MalformedEnvelope,
/// A verifier backend rejected the proof.
#[error("{kind:?} verifier backend rejected proof: {reason:?}")]
BackendRejected {
/// Proof kind rejected by the backend.
kind: ProofKind,
/// Backend rejection code.
reason: VerificationFailureCode,
},
}

/// A proof-shaped envelope whose current validation admits replay-trace
/// evidence by checking structure and public-input binding.
#[derive(Clone, Debug, PartialEq, Eq)]
Expand All @@ -62,22 +103,20 @@ impl ProofEnvelope {
///
/// # Errors
///
/// Returns a validation error string if proof bytes are empty or public inputs mismatch.
pub fn validate_shape(&self, expected_public_inputs_hash: Hash) -> Result<(), String> {
/// Returns [`ProofError`] if proof bytes are empty, the proof kind is not
/// admitted by shape-only validation, or public inputs mismatch.
pub fn validate_shape(&self, expected_public_inputs_hash: Hash) -> Result<(), ProofError> {
if !self.kind.accepts_shape_only() {
return Err(format!(
"{:?} proof envelopes require a verifier backend before admission",
self.kind
));
return Err(ProofError::UnsupportedKind { kind: self.kind });
}
if self.proof_bytes.is_empty() {
return Err("Proof payload is empty".to_string());
return Err(ProofError::EmptyPayload);
}
if self.public_inputs_hash != expected_public_inputs_hash {
return Err(format!(
"Public inputs mismatch: expected {:?}, got {:?}",
expected_public_inputs_hash, self.public_inputs_hash
));
return Err(ProofError::PublicInputsMismatch {
expected: expected_public_inputs_hash,
actual: self.public_inputs_hash,
});
}
Ok(())
}
Expand Down
Loading
Loading