use serde::{Deserialize, Serialize};
use crate::abi::{EntityId, InstanceId, Principal, Tick, TypeCode};
use crate::state::ScheduledActionId;
use super::signature::{SignatureClass, VerifierClass};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TypeRegistryPin {
pub type_code: TypeCode,
pub schema_hash: [u8; 32],
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WalHeader {
pub magic: [u8; 8],
pub kernel_semver: (u16, u16, u16),
pub postcard_version: u32,
pub blake3_version: u32,
pub domain_separation_context: Vec<u8>,
pub world_id: [u8; 32],
pub abi_version: (u16, u16),
pub manifest_digest: [u8; 32],
pub type_registry_pins: Vec<TypeRegistryPin>,
pub verifying_key: Option<[u8; 32]>,
pub verifying_key_pqc: Option<Vec<u8>>,
}
impl WalHeader {
pub const MAGIC: [u8; 8] = *b"ARKHEWAL";
pub const CURRENT_KERNEL_SEMVER: (u16, u16, u16) = (0, 15, 0);
pub const ABI_VERSION: (u16, u16) = (0, 15);
pub const POSTCARD_MAJOR: u32 = 1;
pub const BLAKE3_MAJOR: u32 = 1;
pub const DOMAIN_CTX: &'static [u8] = b"arkhe-kernel v0.15 WAL chain domain separation context";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalRecordKind {
Submit,
Step,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepVerdict {
Committed,
AuthDenied,
BudgetPartial {
denied: u32,
},
Skipped {
reason: SkipReason,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SkipReason {
Unregistered,
DeserFailed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WalRecordContent {
Submit {
seq: u64,
instance: InstanceId,
principal: Principal,
actor: Option<EntityId>,
caps_at_submit: u64,
at: Tick,
action_type_code: TypeCode,
action_bytes: Vec<u8>,
allocated_id: ScheduledActionId,
},
Step {
seq: u64,
instance: InstanceId,
popped_id: ScheduledActionId,
now: Tick,
session_caps: u64,
verdict: StepVerdict,
post_state_digest: [u8; 32],
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalRecord {
pub content: WalRecordContent,
pub prev_chain_hash: [u8; 32],
pub this_chain_hash: [u8; 32],
pub signature: Option<Vec<u8>>,
pub signature_pqc: Option<Vec<u8>>,
}
impl WalRecord {
pub fn seq(&self) -> u64 {
match &self.content {
WalRecordContent::Submit { seq, .. } | WalRecordContent::Step { seq, .. } => *seq,
}
}
pub fn kind(&self) -> WalRecordKind {
match &self.content {
WalRecordContent::Submit { .. } => WalRecordKind::Submit,
WalRecordContent::Step { .. } => WalRecordKind::Step,
}
}
}
#[derive(Serialize)]
struct WalRecordBody<'a> {
content: WalRecordBodyContent<'a>,
prev_chain_hash: [u8; 32],
}
#[derive(Serialize)]
enum WalRecordBodyContent<'a> {
Submit {
seq: u64,
instance: InstanceId,
principal: &'a Principal,
actor: Option<EntityId>,
caps_at_submit: u64,
at: Tick,
action_type_code: TypeCode,
action_bytes: &'a [u8],
allocated_id: ScheduledActionId,
},
Step {
seq: u64,
instance: InstanceId,
popped_id: ScheduledActionId,
now: Tick,
session_caps: u64,
verdict: StepVerdict,
post_state_digest: [u8; 32],
},
}
impl<'a> WalRecordBody<'a> {
fn from_record(rec: &'a WalRecord, prev: [u8; 32]) -> Self {
Self::from_content(&rec.content, prev)
}
fn from_content(content_ref: &'a WalRecordContent, prev: [u8; 32]) -> Self {
let content = match content_ref {
WalRecordContent::Submit {
seq,
instance,
principal,
actor,
caps_at_submit,
at,
action_type_code,
action_bytes,
allocated_id,
} => WalRecordBodyContent::Submit {
seq: *seq,
instance: *instance,
principal,
actor: *actor,
caps_at_submit: *caps_at_submit,
at: *at,
action_type_code: *action_type_code,
action_bytes,
allocated_id: *allocated_id,
},
WalRecordContent::Step {
seq,
instance,
popped_id,
now,
session_caps,
verdict,
post_state_digest,
} => WalRecordBodyContent::Step {
seq: *seq,
instance: *instance,
popped_id: *popped_id,
now: *now,
session_caps: *session_caps,
verdict: *verdict,
post_state_digest: *post_state_digest,
},
};
Self {
content,
prev_chain_hash: prev,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Wal {
pub header: WalHeader,
pub records: Vec<WalRecord>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SignatureTier {
None,
Ed25519,
Hybrid,
}
#[derive(Debug, Clone, Default)]
pub struct TrustAnchor {
pub min_tier: Option<SignatureTier>,
pub ed25519_verifying_key: Option<[u8; 32]>,
pub mldsa_verifying_key: Option<Vec<u8>>,
pub expected_manifest_digest: Option<[u8; 32]>,
pub expected_chain_tip: Option<[u8; 32]>,
}
pub struct WalWriter {
header: WalHeader,
records: Vec<WalRecord>,
next_seq: u64,
prev_hash: [u8; 32],
chain_key: [u8; 32],
sig_class: SignatureClass,
}
type SealedRecordParts = ([u8; 32], Option<Vec<u8>>, Option<Vec<u8>>);
fn build_chain_key(world_id: &[u8; 32]) -> [u8; 32] {
let ctx = core::str::from_utf8(WalHeader::DOMAIN_CTX).expect("DOMAIN_CTX is valid UTF-8 ASCII");
blake3::derive_key(ctx, world_id)
}
fn build_dsc() -> Vec<u8> {
WalHeader::DOMAIN_CTX.to_vec()
}
impl WalWriter {
pub fn new(world_id: [u8; 32], manifest_digest: [u8; 32]) -> Self {
Self::with_signature(world_id, manifest_digest, SignatureClass::None)
}
pub fn with_signature(
world_id: [u8; 32],
manifest_digest: [u8; 32],
sig_class: SignatureClass,
) -> Self {
let chain_key = build_chain_key(&world_id);
let header = WalHeader {
magic: WalHeader::MAGIC,
kernel_semver: WalHeader::CURRENT_KERNEL_SEMVER,
postcard_version: WalHeader::POSTCARD_MAJOR,
blake3_version: WalHeader::BLAKE3_MAJOR,
domain_separation_context: build_dsc(),
world_id,
abi_version: WalHeader::ABI_VERSION,
manifest_digest,
type_registry_pins: Vec::new(),
verifying_key: sig_class.verifying_key_bytes(),
verifying_key_pqc: sig_class.verifying_key_pqc_bytes(),
};
Self {
header,
records: Vec::new(),
next_seq: 0,
prev_hash: [0u8; 32],
chain_key,
sig_class,
}
}
pub(crate) fn rebuild_from_header(header: &WalHeader) -> Self {
let chain_key = build_chain_key(&header.world_id);
Self {
header: header.clone(),
records: Vec::new(),
next_seq: 0,
prev_hash: [0u8; 32],
chain_key,
sig_class: SignatureClass::None,
}
}
fn seal(&self, body: &WalRecordBody) -> Result<SealedRecordParts, WalError> {
let body_bytes =
postcard::to_allocvec(body).map_err(|e| WalError::SerializeFailed(format!("{}", e)))?;
let mut hasher = blake3::Hasher::new_keyed(&self.chain_key);
hasher.update(&self.prev_hash);
hasher.update(&body_bytes);
let this_hash: [u8; 32] = *hasher.finalize().as_bytes();
let (signature, signature_pqc) = match self.sig_class.sign_hybrid(&body_bytes) {
Some(hyb) => (Some(hyb.ed25519.to_vec()), Some(hyb.pqc)),
None => (self.sig_class.sign(&body_bytes).map(|s| s.to_vec()), None),
};
Ok((this_hash, signature, signature_pqc))
}
fn push_sealed(&mut self, content: WalRecordContent) -> Result<&WalRecord, WalError> {
let prev = self.prev_hash;
let (this_hash, signature, signature_pqc) = {
let body = WalRecordBody::from_content(&content, prev);
self.seal(&body)?
};
let record = WalRecord {
content,
prev_chain_hash: prev,
this_chain_hash: this_hash,
signature,
signature_pqc,
};
self.records.push(record);
self.prev_hash = this_hash;
Ok(self.records.last().expect("just pushed"))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn append_submit(
&mut self,
instance: InstanceId,
principal: Principal,
actor: Option<EntityId>,
caps_at_submit: u64,
at: Tick,
action_type_code: TypeCode,
action_bytes: Vec<u8>,
allocated_id: ScheduledActionId,
) -> Result<&WalRecord, WalError> {
self.next_seq = self.next_seq.saturating_add(1);
let seq = self.next_seq;
let content = WalRecordContent::Submit {
seq,
instance,
principal,
actor,
caps_at_submit,
at,
action_type_code,
action_bytes,
allocated_id,
};
self.push_sealed(content)
}
pub(crate) fn append_step(
&mut self,
instance: InstanceId,
popped_id: ScheduledActionId,
now: Tick,
session_caps: u64,
verdict: StepVerdict,
post_state_digest: [u8; 32],
) -> Result<&WalRecord, WalError> {
self.next_seq = self.next_seq.saturating_add(1);
let seq = self.next_seq;
let content = WalRecordContent::Step {
seq,
instance,
popped_id,
now,
session_caps,
verdict,
post_state_digest,
};
self.push_sealed(content)
}
pub fn header(&self) -> &WalHeader {
&self.header
}
pub fn records(&self) -> &[WalRecord] {
&self.records
}
pub fn chain_tip(&self) -> [u8; 32] {
self.prev_hash
}
pub fn record_count(&self) -> usize {
self.records.len()
}
}
impl Wal {
pub fn from_writer(w: WalWriter) -> Self {
Self {
header: w.header,
records: w.records,
}
}
pub fn serialize(&self) -> Result<Vec<u8>, WalError> {
postcard::to_allocvec(self).map_err(|e| WalError::SerializeFailed(format!("{}", e)))
}
pub fn deserialize(bytes: &[u8]) -> Result<Self, WalError> {
postcard::from_bytes(bytes).map_err(|e| WalError::DeserializeFailed(format!("{}", e)))
}
pub fn chain_tip(&self) -> [u8; 32] {
self.records
.last()
.map(|r| r.this_chain_hash)
.unwrap_or([0u8; 32])
}
pub fn verify_chain(&self, world_id: [u8; 32]) -> Result<(), WalError> {
let chain_key = build_chain_key(&world_id);
let verifier = VerifierClass::from_header_bytes(
self.header.verifying_key.as_ref(),
self.header.verifying_key_pqc.as_deref(),
)
.map_err(|e| match e {
crate::persist::signature::VerifierInitError::InvalidEd25519Key
| crate::persist::signature::VerifierInitError::InvalidPqcKey => {
WalError::InvalidVerifyingKey
}
crate::persist::signature::VerifierInitError::PqcWithoutEd25519 => {
WalError::PqcWithoutEd25519
}
})?;
let mut prev = [0u8; 32];
for (i, rec) in self.records.iter().enumerate() {
if blake3::Hash::from(rec.prev_chain_hash) != blake3::Hash::from(prev) {
return Err(WalError::ChainBroken { at_record: i });
}
let body = WalRecordBody::from_record(rec, prev);
let body_bytes = postcard::to_allocvec(&body)
.map_err(|e| WalError::SerializeFailed(format!("{}", e)))?;
let mut hasher = blake3::Hasher::new_keyed(&chain_key);
hasher.update(&prev);
hasher.update(&body_bytes);
let computed: [u8; 32] = *hasher.finalize().as_bytes();
if blake3::Hash::from(computed) != blake3::Hash::from(rec.this_chain_hash) {
return Err(WalError::HashMismatch { at_record: i });
}
match &verifier {
VerifierClass::None => {}
VerifierClass::Ed25519(_) => {
let sig_vec = rec
.signature
.as_ref()
.ok_or(WalError::MissingSignature { at_record: i })?;
verifier
.verify(&body_bytes, sig_vec)
.map_err(|_| WalError::SignatureMismatch { at_record: i })?;
}
VerifierClass::Hybrid { .. } => {
let sig_vec = rec
.signature
.as_ref()
.ok_or(WalError::MissingSignature { at_record: i })?;
let sig_pqc = rec
.signature_pqc
.as_ref()
.ok_or(WalError::MissingPqcSignature { at_record: i })?;
verifier
.verify_hybrid(&body_bytes, sig_vec, sig_pqc)
.map_err(|_| WalError::PqcSignatureMismatch { at_record: i })?;
}
}
prev = computed;
}
Ok(())
}
fn header_tier(&self) -> Result<SignatureTier, WalError> {
match (
self.header.verifying_key.is_some(),
self.header.verifying_key_pqc.is_some(),
) {
(false, false) => Ok(SignatureTier::None),
(true, false) => Ok(SignatureTier::Ed25519),
(true, true) => Ok(SignatureTier::Hybrid),
(false, true) => Err(WalError::PqcWithoutEd25519),
}
}
pub fn verify_chain_anchored(
&self,
world_id: [u8; 32],
anchor: &TrustAnchor,
) -> Result<(), WalError> {
let tier = self.header_tier()?;
if let Some(floor) = anchor.min_tier {
if tier < floor {
return Err(WalError::TierDowngrade);
}
}
if let Some(expected) = anchor.ed25519_verifying_key {
match self.header.verifying_key {
Some(k) if k == expected => {}
_ => return Err(WalError::VerifyingKeyMismatch),
}
}
if let Some(expected) = anchor.mldsa_verifying_key.as_deref() {
match self.header.verifying_key_pqc.as_deref() {
Some(k) if k == expected => {}
_ => return Err(WalError::VerifyingKeyMismatch),
}
}
if let Some(expected) = anchor.expected_manifest_digest {
if self.header.manifest_digest != expected {
return Err(WalError::ManifestDigestMismatch);
}
}
self.verify_chain(world_id)?;
if let Some(expected_tip) = anchor.expected_chain_tip {
if self.chain_tip() != expected_tip {
return Err(WalError::ChainTipMismatch);
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum WalError {
SerializeFailed(String),
DeserializeFailed(String),
ChainBroken {
at_record: usize,
},
HashMismatch {
at_record: usize,
},
HeaderIncompatible(String),
InvalidVerifyingKey,
MissingSignature {
at_record: usize,
},
SignatureMismatch {
at_record: usize,
},
MissingPqcSignature {
at_record: usize,
},
PqcSignatureMismatch {
at_record: usize,
},
PqcWithoutEd25519,
TierDowngrade,
VerifyingKeyMismatch,
ManifestDigestMismatch,
ChainTipMismatch,
}
impl core::fmt::Display for WalError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::SerializeFailed(m) => write!(f, "wal serialize failed: {}", m),
Self::DeserializeFailed(m) => write!(f, "wal deserialize failed: {}", m),
Self::ChainBroken { at_record } => {
write!(f, "wal chain broken at record {}", at_record)
}
Self::HashMismatch { at_record } => {
write!(f, "wal hash mismatch at record {}", at_record)
}
Self::HeaderIncompatible(m) => write!(f, "wal header incompatible: {}", m),
Self::InvalidVerifyingKey => write!(
f,
"wal verifying_key invalid (not a valid Ed25519 public key)"
),
Self::MissingSignature { at_record } => {
write!(f, "wal signature missing at record {}", at_record)
}
Self::SignatureMismatch { at_record } => {
write!(f, "wal signature mismatch at record {}", at_record)
}
Self::MissingPqcSignature { at_record } => {
write!(f, "wal PQC signature missing at record {}", at_record)
}
Self::PqcSignatureMismatch { at_record } => {
write!(f, "wal PQC signature mismatch at record {}", at_record)
}
Self::PqcWithoutEd25519 => write!(
f,
"wal envelope invalid (verifying_key_pqc set without verifying_key)"
),
Self::TierDowngrade => write!(
f,
"wal signature tier downgraded below the trust anchor's minimum"
),
Self::VerifyingKeyMismatch => write!(
f,
"wal verifying key does not match the trust anchor's expected key"
),
Self::ManifestDigestMismatch => {
write!(f, "wal manifest_digest does not match the expected digest")
}
Self::ChainTipMismatch => {
write!(f, "wal chain tip does not match the expected tip (tail truncation?)")
}
}
}
}
impl std::error::Error for WalError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::{EntityId, ExternalId, InstanceId, Principal, RouteId, Tick, TypeCode};
use crate::state::ScheduledActionId;
fn world() -> [u8; 32] {
[7u8; 32]
}
fn manifest() -> [u8; 32] {
[3u8; 32]
}
fn sid(n: u64) -> ScheduledActionId {
ScheduledActionId::new(n).unwrap()
}
fn submit_one(w: &mut WalWriter) {
w.append_submit(
InstanceId::new(1).unwrap(),
Principal::System,
None,
0xFF,
Tick(0),
TypeCode(100),
vec![1, 2, 3],
sid(1),
)
.unwrap();
}
fn step_n(w: &mut WalWriter, n: u8) {
w.append_step(
InstanceId::new(1).unwrap(),
sid(1),
Tick(n as u64),
0xFF,
StepVerdict::Committed,
[n; 32],
)
.unwrap();
}
fn append_one(w: &mut WalWriter) {
step_n(w, 1);
}
#[test]
fn empty_writer_serializes_and_deserializes() {
let w = WalWriter::new(world(), manifest());
let wal = Wal::from_writer(w);
let bytes = wal.serialize().unwrap();
let back = Wal::deserialize(&bytes).unwrap();
assert_eq!(back.header, wal.header);
assert_eq!(back.records.len(), 0);
assert_eq!(back.chain_tip(), [0u8; 32]);
}
#[test]
fn single_step_append_produces_nonzero_chain_tip() {
let mut w = WalWriter::new(world(), manifest());
append_one(&mut w);
let tip = w.chain_tip();
assert_ne!(tip, [0u8; 32]);
assert_eq!(w.record_count(), 1);
}
#[test]
fn multi_record_chain_links_each_record() {
let mut w = WalWriter::new(world(), manifest());
for i in 0..5 {
step_n(&mut w, i);
}
let wal = Wal::from_writer(w);
assert_eq!(wal.records.len(), 5);
let mut prev = [0u8; 32];
for rec in &wal.records {
assert_eq!(rec.prev_chain_hash, prev);
prev = rec.this_chain_hash;
}
wal.verify_chain(world()).expect("clean chain");
}
#[test]
fn submit_and_step_chain_verifies() {
let mut w = WalWriter::new(world(), manifest());
submit_one(&mut w);
step_n(&mut w, 2);
let wal = Wal::from_writer(w);
assert_eq!(wal.records.len(), 2);
assert!(matches!(wal.records[0].kind(), WalRecordKind::Submit));
assert!(matches!(wal.records[1].kind(), WalRecordKind::Step));
assert_eq!(wal.records[0].seq(), 1);
assert_eq!(wal.records[1].seq(), 2);
wal.verify_chain(world()).expect("clean chain");
}
#[test]
fn tampered_step_body_breaks_verify_chain() {
let mut w = WalWriter::new(world(), manifest());
for i in 0..3 {
step_n(&mut w, i);
}
let mut wal = Wal::from_writer(w);
if let WalRecordContent::Step {
post_state_digest, ..
} = &mut wal.records[1].content
{
post_state_digest[0] ^= 0xFF;
}
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::HashMismatch { .. })
));
}
#[test]
fn verify_chain_detects_broken_prev_link() {
let mut w = WalWriter::new(world(), manifest());
for i in 0..3 {
step_n(&mut w, i);
}
let mut wal = Wal::from_writer(w);
wal.records[1].prev_chain_hash[0] ^= 1;
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::ChainBroken { at_record: 1 })
));
}
#[test]
fn different_world_id_produces_different_chain() {
let mut w1 = WalWriter::new([1u8; 32], manifest());
let mut w2 = WalWriter::new([2u8; 32], manifest());
append_one(&mut w1);
append_one(&mut w2);
assert_ne!(w1.chain_tip(), w2.chain_tip());
}
#[test]
fn verify_chain_against_wrong_world_id_fails() {
let mut w = WalWriter::new(world(), manifest());
append_one(&mut w);
let wal = Wal::from_writer(w);
assert!(matches!(
wal.verify_chain([99u8; 32]),
Err(WalError::HashMismatch { .. })
));
}
#[test]
fn step_verdict_round_trips() {
let mut w = WalWriter::new(world(), manifest());
w.append_step(
InstanceId::new(1).unwrap(),
sid(1),
Tick(0),
0xFF,
StepVerdict::BudgetPartial { denied: 3 },
[4u8; 32],
)
.unwrap();
let wal = Wal::from_writer(w);
let bytes = wal.serialize().unwrap();
let back = Wal::deserialize(&bytes).unwrap();
match &back.records[0].content {
WalRecordContent::Step { verdict, .. } => {
assert_eq!(*verdict, StepVerdict::BudgetPartial { denied: 3 });
}
WalRecordContent::Submit { .. } => panic!("expected Step"),
}
}
#[test]
fn submit_record_round_trips_inputs() {
let actor = Some(EntityId::new(42).unwrap());
let mut w = WalWriter::new(world(), manifest());
w.append_submit(
InstanceId::new(9).unwrap(),
Principal::External(ExternalId(7)),
actor,
0xDEAD_BEEF,
Tick(5),
TypeCode(101),
vec![9, 8, 7],
sid(3),
)
.unwrap();
let wal = Wal::from_writer(w);
let bytes = wal.serialize().unwrap();
let back = Wal::deserialize(&bytes).unwrap();
match &back.records[0].content {
WalRecordContent::Submit {
instance,
actor: a,
caps_at_submit,
allocated_id,
action_bytes,
..
} => {
assert_eq!(*instance, InstanceId::new(9).unwrap());
assert_eq!(*a, actor);
assert_eq!(*caps_at_submit, 0xDEAD_BEEF);
assert_eq!(*allocated_id, sid(3));
assert_eq!(action_bytes, &vec![9, 8, 7]);
}
WalRecordContent::Step { .. } => panic!("expected Submit"),
}
assert!(back.verify_chain(world()).is_ok());
}
#[test]
fn header_carries_magic_and_versions() {
let h = WalWriter::new(world(), manifest()).header().clone();
assert_eq!(h.magic, *b"ARKHEWAL");
assert_eq!(h.kernel_semver, (0, 15, 0));
assert_eq!(h.abi_version, (0, 15));
assert_eq!(h.world_id, world());
assert_eq!(h.manifest_digest, manifest());
assert!(h.type_registry_pins.is_empty());
assert!(h.verifying_key.is_none());
let _ = RouteId(1);
}
#[test]
fn signature_class_none_produces_no_signature() {
let mut w = WalWriter::new(world(), manifest());
append_one(&mut w);
let wal = Wal::from_writer(w);
assert!(wal.header.verifying_key.is_none());
assert!(wal.records[0].signature.is_none());
wal.verify_chain(world())
.expect("Tier 1 chain still verifies");
}
#[test]
fn signature_class_ed25519_signs_each_record() {
let sig_class = SignatureClass::new_ed25519_from_secret([7u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
for _ in 0..3 {
append_one(&mut w);
}
let wal = Wal::from_writer(w);
assert!(wal.header.verifying_key.is_some());
assert_eq!(wal.records.len(), 3);
for rec in &wal.records {
let sig = rec.signature.as_ref().expect("Ed25519 signs every record");
assert_eq!(sig.len(), 64);
}
}
#[test]
fn verify_chain_validates_signatures() {
let sig_class = SignatureClass::new_ed25519_from_secret([11u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
for _ in 0..3 {
append_one(&mut w);
}
let wal = Wal::from_writer(w);
let bytes = wal.serialize().unwrap();
let back = Wal::deserialize(&bytes).unwrap();
back.verify_chain(world()).expect("signed chain verifies");
}
#[test]
fn tampered_signature_fails_verify() {
let sig_class = SignatureClass::new_ed25519_from_secret([13u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
append_one(&mut w);
append_one(&mut w);
let mut wal = Wal::from_writer(w);
if let Some(sig) = wal.records[1].signature.as_mut() {
sig[0] ^= 0xFF;
}
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::SignatureMismatch { at_record: 1 })
));
}
#[test]
fn missing_signature_fails_verify_when_header_has_key() {
let sig_class = SignatureClass::new_ed25519_from_secret([17u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
append_one(&mut w);
let mut wal = Wal::from_writer(w);
wal.records[0].signature = None;
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::MissingSignature { at_record: 0 })
));
}
#[test]
fn wrong_key_fails_verify() {
let sig_class = SignatureClass::new_ed25519_from_secret([19u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
append_one(&mut w);
let mut wal = Wal::from_writer(w);
let other = SignatureClass::new_ed25519_from_secret([23u8; 32])
.verifying_key_bytes()
.unwrap();
wal.header.verifying_key = Some(other);
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::SignatureMismatch { at_record: 0 })
));
}
#[test]
fn signature_deterministic_across_runs() {
let mk = |secret: [u8; 32]| -> Vec<Vec<u8>> {
let mut w = WalWriter::with_signature(
world(),
manifest(),
SignatureClass::new_ed25519_from_secret(secret),
);
append_one(&mut w);
append_one(&mut w);
let wal = Wal::from_writer(w);
wal.records
.iter()
.map(|r| r.signature.clone().unwrap())
.collect()
};
let sigs1 = mk([29u8; 32]);
let sigs2 = mk([29u8; 32]);
assert_eq!(sigs1, sigs2);
assert_eq!(sigs1[0].len(), 64);
}
#[test]
fn domain_ctx_byte_identity_blake3() {
const EXPECTED: &[u8] = b"arkhe-kernel v0.15 WAL chain domain separation context";
assert_eq!(WalHeader::DOMAIN_CTX, EXPECTED);
assert_eq!(WalHeader::DOMAIN_CTX.len(), 54);
const FROZEN_HEX: &str = "3e1aa9478e76820fbdb31ca8fdf136df81d83635e2925584934cfa110f682129";
let actual_hex = blake3::hash(WalHeader::DOMAIN_CTX).to_hex();
assert_eq!(
actual_hex.as_str(),
FROZEN_HEX,
"DOMAIN_CTX BLAKE3 hash regression — byte-level edit detected",
);
}
#[test]
fn wal_record_persists_actor_for_replay_determinism() {
let actor = Some(EntityId::new(42).unwrap());
let mut w = WalWriter::new(world(), manifest());
w.append_submit(
InstanceId::new(1).unwrap(),
Principal::System,
actor,
0,
Tick(0),
TypeCode(100),
vec![1, 2, 3],
sid(1),
)
.unwrap();
let wal = Wal::from_writer(w);
match &wal.records[0].content {
WalRecordContent::Submit { actor: a, .. } => assert_eq!(*a, actor),
WalRecordContent::Step { .. } => panic!("expected Submit"),
}
assert!(wal.verify_chain(world()).is_ok());
}
#[test]
fn verify_chain_anchored_rejects_downgrade_and_substitution() {
let sig = SignatureClass::new_ed25519_from_secret([7u8; 32]);
let vk = sig.verifying_key_bytes().unwrap();
let mut w = WalWriter::with_signature(world(), manifest(), sig);
append_one(&mut w);
let wal = Wal::from_writer(w);
let good = TrustAnchor {
min_tier: Some(SignatureTier::Ed25519),
ed25519_verifying_key: Some(vk),
..Default::default()
};
assert!(wal.verify_chain_anchored(world(), &good).is_ok());
let downgrade = TrustAnchor {
min_tier: Some(SignatureTier::Hybrid),
..Default::default()
};
assert!(matches!(
wal.verify_chain_anchored(world(), &downgrade),
Err(WalError::TierDowngrade)
));
let wrong_key = TrustAnchor {
min_tier: Some(SignatureTier::Ed25519),
ed25519_verifying_key: Some([0xAB; 32]),
..Default::default()
};
assert!(matches!(
wal.verify_chain_anchored(world(), &wrong_key),
Err(WalError::VerifyingKeyMismatch)
));
}
#[test]
fn submit_record_postcard_layout_byte_identity() {
let mut w = WalWriter::new([7u8; 32], [3u8; 32]);
w.append_submit(
InstanceId::new(99).unwrap(),
Principal::System,
Some(EntityId::new(5).unwrap()),
0xCAFE,
Tick(42),
TypeCode(0xBEEF),
vec![0xAA, 0xBB, 0xCC],
sid(7),
)
.unwrap();
let wal = Wal::from_writer(w);
let encoded = postcard::to_allocvec(&wal.records[0]).expect("postcard encode");
const FROZEN_HEX: &str = "43e10a825aa2b78cbbe59114fc546c31f64c8ffdf00dada54127bdc598967562";
assert_eq!(
blake3::hash(&encoded).to_hex().as_str(),
FROZEN_HEX,
"Submit record postcard byte sequence regression",
);
}
#[test]
fn step_record_postcard_layout_byte_identity() {
let mut w = WalWriter::new([7u8; 32], [3u8; 32]);
w.append_step(
InstanceId::new(99).unwrap(),
sid(13),
Tick(42),
0xC0FFEE,
StepVerdict::BudgetPartial { denied: 2 },
[0x5A; 32],
)
.unwrap();
let wal = Wal::from_writer(w);
let encoded = postcard::to_allocvec(&wal.records[0]).expect("postcard encode");
const FROZEN_HEX: &str = "44a440370648b160fa342075d77a1fd72629c1fde3467c0458c34c36d2ae4aa9";
assert_eq!(
blake3::hash(&encoded).to_hex().as_str(),
FROZEN_HEX,
"Step record postcard byte sequence regression",
);
}
#[test]
fn chain_hash_frozen_for_step_record() {
let mut w = WalWriter::new([7u8; 32], [3u8; 32]);
w.append_step(
InstanceId::new(1).unwrap(),
sid(1),
Tick(0),
0xFF,
StepVerdict::Committed,
[9u8; 32],
)
.unwrap();
let wal = Wal::from_writer(w);
const FROZEN_HEX: &str = "0627f6472e44e5af82b9019698b80adc08fe02a5ffcf081b79a3ea824556df14";
assert_eq!(
blake3::Hash::from(wal.records[0].this_chain_hash)
.to_hex()
.as_str(),
FROZEN_HEX,
"chain hash regression — DOMAIN_CTX or Step body field order changed",
);
}
#[test]
fn wal_record_hybrid_layout_byte_identity() {
let sig_class = SignatureClass::new_ed25519_from_secret([19u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
append_one(&mut w);
let mut wal = Wal::from_writer(w);
let baseline_encoded = postcard::to_allocvec(&wal.records[0]).expect("baseline encode");
wal.records[0].signature_pqc = Some(vec![0xAB; 3309]);
let with_pqc_encoded = postcard::to_allocvec(&wal.records[0]).expect("with_pqc encode");
assert_eq!(
with_pqc_encoded.len() - baseline_encoded.len(),
3311,
"PQC signature envelope size mismatch — ML-DSA 65 must fit",
);
}
#[test]
fn wal_header_verifying_key_pqc_slot_pinned() {
let h = WalWriter::new(world(), manifest()).header().clone();
assert!(h.verifying_key.is_none());
assert!(h.verifying_key_pqc.is_none());
let mut h_pqc = h.clone();
h_pqc.verifying_key_pqc = Some(vec![0xCD; 1952]);
let baseline = postcard::to_allocvec(&h).expect("encode baseline");
let with_pqc = postcard::to_allocvec(&h_pqc).expect("encode with pqc key");
assert_eq!(
with_pqc.len() - baseline.len(),
1954,
"PQC verifying-key envelope size mismatch — ML-DSA 65 must fit",
);
}
#[test]
fn hybrid_writer_emits_both_signatures() {
let sig_class = SignatureClass::new_hybrid_from_secrets([7u8; 32], [11u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
for _ in 0..3 {
append_one(&mut w);
}
let wal = Wal::from_writer(w);
assert_eq!(wal.header.verifying_key.expect("Hybrid pins Ed25519 vk").len(), 32);
assert_eq!(
wal.header
.verifying_key_pqc
.as_ref()
.expect("Hybrid pins PQC vk")
.len(),
1952
);
assert_eq!(wal.records.len(), 3);
for rec in &wal.records {
assert_eq!(
rec.signature
.as_ref()
.expect("Hybrid signs Ed25519 every record")
.len(),
64
);
assert_eq!(
rec.signature_pqc
.as_ref()
.expect("Hybrid signs PQC every record")
.len(),
3309
);
}
}
#[test]
fn hybrid_verify_chain_and_mode_passes_with_both_valid() {
let sig_class = SignatureClass::new_hybrid_from_secrets([13u8; 32], [17u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
for _ in 0..3 {
append_one(&mut w);
}
let wal = Wal::from_writer(w);
let bytes = wal.serialize().unwrap();
let back = Wal::deserialize(&bytes).unwrap();
back.verify_chain(world())
.expect("Hybrid signed chain verifies (AND-mode pass)");
}
#[test]
fn hybrid_verify_chain_rejects_missing_pqc() {
let sig_class = SignatureClass::new_hybrid_from_secrets([19u8; 32], [23u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
append_one(&mut w);
let mut wal = Wal::from_writer(w);
wal.records[0].signature_pqc = None;
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::MissingPqcSignature { at_record: 0 })
));
}
#[test]
fn hybrid_verify_chain_rejects_corrupt_pqc_signature() {
let sig_class = SignatureClass::new_hybrid_from_secrets([29u8; 32], [31u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
append_one(&mut w);
append_one(&mut w);
let mut wal = Wal::from_writer(w);
if let Some(sig_pqc) = wal.records[1].signature_pqc.as_mut() {
sig_pqc[0] ^= 0xFF;
}
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::PqcSignatureMismatch { at_record: 1 })
));
}
#[test]
fn hybrid_verify_chain_rejects_corrupt_ed25519_when_pqc_valid() {
let sig_class = SignatureClass::new_hybrid_from_secrets([37u8; 32], [41u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
append_one(&mut w);
append_one(&mut w);
let mut wal = Wal::from_writer(w);
if let Some(sig) = wal.records[0].signature.as_mut() {
sig[0] ^= 0xFF;
}
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::PqcSignatureMismatch { at_record: 0 })
));
}
#[test]
fn ed25519_only_wal_replays_under_hybrid_kernel() {
let sig_class = SignatureClass::new_ed25519_from_secret([43u8; 32]);
let mut w = WalWriter::with_signature(world(), manifest(), sig_class);
for _ in 0..3 {
append_one(&mut w);
}
let wal = Wal::from_writer(w);
assert!(wal.header.verifying_key.is_some());
assert!(wal.header.verifying_key_pqc.is_none());
for rec in &wal.records {
assert!(rec.signature.is_some());
assert!(rec.signature_pqc.is_none());
}
let bytes = wal.serialize().unwrap();
let back = Wal::deserialize(&bytes).unwrap();
back.verify_chain(world())
.expect("Ed25519-only WAL replays under Hybrid-capable kernel");
}
#[test]
fn pqc_without_ed25519_envelope_rejected() {
let sig_class = SignatureClass::new_hybrid_from_secrets([47u8; 32], [53u8; 32]);
let w = WalWriter::with_signature(world(), manifest(), sig_class);
let mut wal = Wal::from_writer(w);
wal.header.verifying_key = None;
assert!(matches!(
wal.verify_chain(world()),
Err(WalError::PqcWithoutEd25519)
));
}
}