From aca7f6b197ebd2afae02e81a9ca3b4e84762fc21 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 00:13:44 +0000 Subject: [PATCH] feat(rvf): integrate publishable acceptance test with native SHAKE-256 witness chain Replace standalone SHA-256 chain with rvf-crypto SHAKE-256, add native .rvf binary output (WITNESS_SEG + META_SEG), and wire witness verification into rvf-wasm microkernel. Key changes: - Feature-gate ed25519 in rvf-crypto for WASM compatibility (sha3 no_std) - Rewrite WitnessChainBuilder to use shake256_256 + parallel rvf_crypto::WitnessEntry - Add export_rvf_binary() with WITNESS_SEG (0x0A) + META_SEG (0x07) segments - Add rvf_witness_verify/rvf_witness_count exports to rvf-wasm - Add verify-rvf subcommand to acceptance-rvf CLI - Write ADR-037 documenting architecture and AGI benchmark integration - Update rvf-crypto, rvf-wasm, and rvf READMEs 86 tests pass (66 lib + 20 integration). rvf-crypto 49 tests pass. https://claude.ai/code/session_01RnwD4x5cbpB7FPvoyYQz8G --- Cargo.lock | 3 + crates/rvf/Cargo.lock | 1 + crates/rvf/README.md | 2 +- crates/rvf/rvf-crypto/Cargo.toml | 9 +- crates/rvf/rvf-crypto/README.md | 8 + crates/rvf/rvf-crypto/src/lib.rs | 2 + crates/rvf/rvf-wasm/Cargo.toml | 1 + crates/rvf/rvf-wasm/README.md | 11 + crates/rvf/rvf-wasm/src/lib.rs | 54 +++++ ...ADR-037-publishable-rvf-acceptance-test.md | 111 ++++++++++ examples/benchmarks/Cargo.toml | 5 + examples/benchmarks/src/bin/acceptance_rvf.rs | 43 +++- examples/benchmarks/src/publishable_rvf.rs | 198 ++++++++++++++++-- 13 files changed, 415 insertions(+), 33 deletions(-) create mode 100644 docs/adr/ADR-037-publishable-rvf-acceptance-test.md diff --git a/Cargo.lock b/Cargo.lock index 6ab4defbb..98437f505 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7932,6 +7932,9 @@ dependencies = [ "rayon", "reqwest 0.11.27", "ruvector-core 2.0.3", + "rvf-crypto", + "rvf-types", + "rvf-wire", "serde", "serde_json", "sha2", diff --git a/crates/rvf/Cargo.lock b/crates/rvf/Cargo.lock index 110c792af..a9b964e1e 100644 --- a/crates/rvf/Cargo.lock +++ b/crates/rvf/Cargo.lock @@ -1821,6 +1821,7 @@ name = "rvf-wasm" version = "0.1.0" dependencies = [ "dlmalloc", + "rvf-crypto", "rvf-types", ] diff --git a/crates/rvf/README.md b/crates/rvf/README.md index aab4d4a15..78c67b6a9 100644 --- a/crates/rvf/README.md +++ b/crates/rvf/README.md @@ -540,7 +540,7 @@ An `.rvf` file is a sequence of 64-byte-aligned segments. Each segment has a sel | `rvf-kernel` | 2,400+ | Real Linux kernel builder, cpio/newc initramfs, Docker build, SHA3-256 verification | | `rvf-launch` | 1,200+ | QEMU microvm launcher, KVM/TCG detection, QMP shutdown protocol | | `rvf-ebpf` | 1,100+ | Real BPF C compiler (XDP, socket filter, TC), vmlinux.h generation | -| `rvf-wasm` | 1,616 | WASM control plane: in-memory store, query, segment inspection (~46 KB) | +| `rvf-wasm` | 1,700+ | WASM control plane: in-memory store, query, segment inspection, witness chain verification (~46 KB) | | `rvf-node` | 852 | Node.js N-API bindings with lineage, kernel/eBPF, and inspection | | `rvf-cli` | 1,800+ | Unified CLI with 17 subcommands (create, ingest, query, delete, status, inspect, compact, derive, serve, launch, embed-kernel, embed-ebpf, filter, freeze, verify-witness, verify-attestation, rebuild-refcounts) | | `rvf-server` | 1,165 | HTTP REST + TCP streaming server | diff --git a/crates/rvf/rvf-crypto/Cargo.toml b/crates/rvf/rvf-crypto/Cargo.toml index d4a82541d..8e7ac16cb 100644 --- a/crates/rvf/rvf-crypto/Cargo.toml +++ b/crates/rvf/rvf-crypto/Cargo.toml @@ -12,13 +12,14 @@ keywords = ["vector", "crypto", "sha3", "ed25519", "rvf"] rust-version = "1.87" [features] -default = ["std"] -std = [] +default = ["std", "ed25519"] +std = ["sha3/std"] +ed25519 = ["dep:ed25519-dalek"] [dependencies] rvf-types = { version = "0.1.0", path = "../rvf-types" } -sha3 = "0.10" -ed25519-dalek = { version = "2", features = ["rand_core"] } +sha3 = { version = "0.10", default-features = false } +ed25519-dalek = { version = "2", features = ["rand_core"], optional = true } [dev-dependencies] rand = "0.8" diff --git a/crates/rvf/rvf-crypto/README.md b/crates/rvf/rvf-crypto/README.md index ad2662f7d..fdcd7e929 100644 --- a/crates/rvf/rvf-crypto/README.md +++ b/crates/rvf/rvf-crypto/README.md @@ -20,6 +20,14 @@ rvf-crypto = "0.1" ## Features - `std` (default) -- enable `std` support +- `ed25519` (default) -- enable Ed25519 signing via `ed25519-dalek` + +For no_std or WASM targets that only need hashing and witness chains (no signing), disable defaults: + +```toml +[dependencies] +rvf-crypto = { version = "0.1", default-features = false } +``` ## Lineage Witness Functions diff --git a/crates/rvf/rvf-crypto/src/lib.rs b/crates/rvf/rvf-crypto/src/lib.rs index e972186f1..85f68e433 100644 --- a/crates/rvf/rvf-crypto/src/lib.rs +++ b/crates/rvf/rvf-crypto/src/lib.rs @@ -9,6 +9,7 @@ extern crate alloc; pub mod footer; pub mod hash; +#[cfg(feature = "ed25519")] pub mod sign; pub mod witness; pub mod attestation; @@ -16,6 +17,7 @@ pub mod lineage; pub use footer::{decode_signature_footer, encode_signature_footer}; pub use hash::{shake256_128, shake256_256, shake256_hash}; +#[cfg(feature = "ed25519")] pub use sign::{sign_segment, verify_segment}; pub use witness::{create_witness_chain, verify_witness_chain, WitnessEntry}; pub use lineage::{ diff --git a/crates/rvf/rvf-wasm/Cargo.toml b/crates/rvf/rvf-wasm/Cargo.toml index 6a7f5a5ed..a77bd8479 100644 --- a/crates/rvf/rvf-wasm/Cargo.toml +++ b/crates/rvf/rvf-wasm/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["cdylib"] [dependencies] rvf-types = { version = "0.1.0", path = "../rvf-types", default-features = false } +rvf-crypto = { version = "0.1.0", path = "../rvf-crypto", default-features = false } dlmalloc = { version = "0.2", features = ["global"] } [profile.release] diff --git a/crates/rvf/rvf-wasm/README.md b/crates/rvf/rvf-wasm/README.md index 7f6f412bf..5efe6c268 100644 --- a/crates/rvf/rvf-wasm/README.md +++ b/crates/rvf/rvf-wasm/README.md @@ -68,6 +68,17 @@ Parse and inspect `.rvf` file structure from raw bytes: | `rvf_segment_info(buf, len, idx, out)` | Get segment details by index | | `rvf_verify_checksum(buf, len)` | Verify CRC32C integrity | +### Witness Chain Verification (2 exports) + +Verify SHAKE-256 witness chains from WITNESS_SEG payloads: + +| Export | Description | +|--------|-------------| +| `rvf_witness_verify(chain_ptr, chain_len) -> i32` | Verify full chain integrity; returns entry count or negative error (-2 = truncated, -3 = hash mismatch) | +| `rvf_witness_count(chain_len) -> i32` | Count entries without full verification (chain_len / 73) | + +These exports enable browser-side verification of acceptance test artifacts and audit trails without any backend. See [ADR-037](../../docs/adr/ADR-037-publishable-rvf-acceptance-test.md). + ### Memory Management (2 exports) | Export | Description | diff --git a/crates/rvf/rvf-wasm/src/lib.rs b/crates/rvf/rvf-wasm/src/lib.rs index 20caa2f04..7688d8eb5 100644 --- a/crates/rvf/rvf-wasm/src/lib.rs +++ b/crates/rvf/rvf-wasm/src/lib.rs @@ -707,6 +707,60 @@ pub extern "C" fn rvf_verify_checksum(buf_ptr: i32, buf_len: i32) -> i32 { } } +// ===================================================================== +// Witness Chain Verification +// ===================================================================== + +/// Verify a SHAKE-256 witness chain in memory. +/// +/// `chain_ptr`: pointer to serialized witness chain (73 bytes per entry). +/// `chain_len`: total byte length of the chain. +/// +/// Returns the number of verified entries on success, or a negative error code: +/// -1: invalid pointer/length +/// -2: truncated chain (not a multiple of 73 bytes) +/// -3: chain integrity failure (prev_hash mismatch) +#[no_mangle] +pub extern "C" fn rvf_witness_verify(chain_ptr: i32, chain_len: i32) -> i32 { + if chain_len < 0 { + return -1; + } + let len = chain_len as usize; + if len == 0 { + return 0; + } + let data = unsafe { core::slice::from_raw_parts(chain_ptr as *const u8, len) }; + match rvf_crypto::verify_witness_chain(data) { + Ok(entries) => entries.len() as i32, + Err(e) => { + use rvf_types::RvfError; + match e { + RvfError::Code(rvf_types::ErrorCode::TruncatedSegment) => -2, + RvfError::Code(rvf_types::ErrorCode::InvalidChecksum) => -3, + _ => -1, + } + } + } +} + +/// Count witness entries in a chain without full verification. +/// +/// Returns the entry count (chain_len / 73), or -1 if not aligned. +#[no_mangle] +pub extern "C" fn rvf_witness_count(chain_len: i32) -> i32 { + if chain_len < 0 { + return -1; + } + let len = chain_len as usize; + if len == 0 { + return 0; + } + if len % 73 != 0 { + return -1; + } + (len / 73) as i32 +} + // ===================================================================== // Memory Management // ===================================================================== diff --git a/docs/adr/ADR-037-publishable-rvf-acceptance-test.md b/docs/adr/ADR-037-publishable-rvf-acceptance-test.md new file mode 100644 index 000000000..d2719aadd --- /dev/null +++ b/docs/adr/ADR-037-publishable-rvf-acceptance-test.md @@ -0,0 +1,111 @@ +# ADR-037: Publishable RVF Acceptance Test + +| Field | Value | +|-------|-------| +| **Status** | Accepted | +| **Date** | 2026-02-16 | +| **Deciders** | RuVector core team | +| **Supersedes** | — | +| **Related** | ADR-029 (RVF canonical format), ADR-032 (RVF WASM integration) | + +## Context + +Temporal reasoning benchmarks produce results that are difficult for external developers to verify independently. Traditional benchmark reports rely on trust: the publisher runs the tests and shares aggregate metrics, but there is no mechanism for a third party to prove that the exact same computations produced those results. This gap matters for publishable research artifacts and for building confidence in the ablation study methodology. + +The RVF format already provides a cryptographic witness chain infrastructure (WITNESS_SEG 0x0A) using SHAKE-256 hash linking, but this capability had not been applied to acceptance testing. + +## Decision + +We integrate the publishable acceptance test directly with the native RVF crate infrastructure to produce a self-contained, offline-verifiable artifact: + +### 1. SHAKE-256 witness chain (rvf-crypto native) + +The acceptance test replaces the standalone SHA-256 chain with `rvf_crypto::shake256_256` for all hash computations. Every puzzle decision (skip mode, context bucket, solve outcome, step count) is hashed into a SHAKE-256 chain where `chain_hash[i] = SHAKE-256(prev_hash || canonical_bytes(record))`. The chain is deterministic: frozen seeds produce identical puzzles, identical solve paths, and identical root hashes. + +The parallel `rvf_crypto::WitnessEntry` list (73 bytes each: `prev_hash[32] + action_hash[32] + timestamp_ns[8] + witness_type[1]`) is built alongside the JSON chain, enabling native `.rvf` binary export. + +### 2. Dual-format output (JSON + .rvf binary) + +The `generate_manifest_with_rvf()` function produces both: + +- **JSON manifest**: Human-readable scorecard, ablation assertions, full witness chain with hex hashes. Suitable for review, CI comparison, and documentation. +- **`.rvf` binary**: A valid RVF file containing: + - `WITNESS_SEG` (0x0A): Native 73-byte entries created by `rvf_crypto::create_witness_chain()`, verifiable by `rvf_crypto::verify_witness_chain()`. + - `META_SEG` (0x07): JSON-encoded scorecards, assertions, and config metadata. + +### 3. WASM witness verification + +Two new exports added to `rvf-wasm`: + +| Export | Signature | Description | +|--------|-----------|-------------| +| `rvf_witness_verify` | `(chain_ptr, chain_len) -> i32` | Verify SHAKE-256 chain integrity. Returns entry count or negative error. | +| `rvf_witness_count` | `(chain_len) -> i32` | Count entries without full verification. | + +This enables browser-side verification of acceptance test `.rvf` files without any backend. + +### 4. Feature-gated ed25519 in rvf-crypto + +To add `rvf-crypto` as a dependency to the no_std WASM microkernel without pulling in the heavy `ed25519-dalek` crate, the `sign` module is now gated behind an `ed25519` feature flag: + +```toml +[features] +default = ["std", "ed25519"] +ed25519 = ["dep:ed25519-dalek"] +``` + +The hash, witness, attestation, lineage, and footer modules remain available without `ed25519`. Existing callers that use default features are unaffected. + +### 5. Three-mode ablation grading + +The acceptance test runs all three ablation modes and asserts six properties: + +| Assertion | Criterion | +|-----------|-----------| +| B beats A on cost | >= 15% cost reduction | +| C beats B on robustness | >= 10% noise accuracy gain | +| Compiler safe | < 5% false-hit rate | +| A skip nonzero | Fixed policy uses skip modes | +| C multi-mode | Learned policy uses >= 2 skip modes | +| C penalty < B penalty | Learned policy reduces early-commit penalty | + +All assertions, per-mode scorecards, and the witness chain root hash are included in the publishable artifact. + +## Verification Protocol + +An external developer reproduces the test: + +```bash +# 1. Generate with default config +cargo run --bin acceptance-rvf -- generate -o manifest.json + +# 2. Compare chain root hash +# If chain_root_hash matches, outcomes are bit-for-bit identical + +# 3. Verify the .rvf binary witness chain +cargo run --bin acceptance-rvf -- verify-rvf -i acceptance_manifest.rvf + +# 4. Or verify in-browser via WASM: +# const count = rvf_witness_verify(chainPtr, chainLen); +``` + +## Consequences + +### Positive + +- External developers can independently verify benchmark outcomes offline +- The `.rvf` binary is compatible with all RVF tooling (CLI, WASM, Node.js) +- Browser-side verification via `rvf_witness_verify` requires zero backend +- Deterministic replay means same config always produces same root hash +- The SHAKE-256 chain is forward-compatible with RVF's attestation infrastructure + +### Negative + +- Switching from SHA-256 to SHAKE-256 changes existing chain root hashes (version bumped to 2) +- The `ed25519` feature gate adds a minor complexity to rvf-crypto's feature matrix +- The WASM binary size increases slightly with the sha3 dependency + +### Neutral + +- JSON and .rvf outputs are independent — either can be used alone +- The `rvf_witness_count` export is a convenience that avoids full verification cost diff --git a/examples/benchmarks/Cargo.toml b/examples/benchmarks/Cargo.toml index be831670a..dd1836989 100644 --- a/examples/benchmarks/Cargo.toml +++ b/examples/benchmarks/Cargo.toml @@ -44,6 +44,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } # Crypto for witness chains sha2 = "0.10" +# RVF native format integration +rvf-types = { path = "../../crates/rvf/rvf-types" } +rvf-crypto = { path = "../../crates/rvf/rvf-crypto" } +rvf-wire = { path = "../../crates/rvf/rvf-wire" } + # Statistics statistical = "1.0" hdrhistogram = "7.5" diff --git a/examples/benchmarks/src/bin/acceptance_rvf.rs b/examples/benchmarks/src/bin/acceptance_rvf.rs index b7c583f4c..a94d045ca 100644 --- a/examples/benchmarks/src/bin/acceptance_rvf.rs +++ b/examples/benchmarks/src/bin/acceptance_rvf.rs @@ -1,10 +1,11 @@ //! Publishable RVF Acceptance Test — CLI entry point. //! //! Generates or verifies a deterministic acceptance test manifest with -//! SHA-256 witness chain. Same seed → same outcomes → same root hash. +//! SHAKE-256 witness chain (rvf-crypto native). Same seed → same outcomes +//! → same root hash. //! //! ```bash -//! # Generate manifest (default config) +//! # Generate manifest (JSON + .rvf binary) //! cargo run --bin acceptance-rvf -- generate -o manifest.json //! //! # Generate with custom config @@ -13,15 +14,20 @@ //! //! # Verify a manifest (re-runs and compares root hash) //! cargo run --bin acceptance-rvf -- verify -i manifest.json +//! +//! # Verify the .rvf binary witness chain +//! cargo run --bin acceptance-rvf -- verify-rvf -i acceptance_manifest.rvf //! ``` use clap::{Parser, Subcommand}; use ruvector_benchmarks::acceptance_test::HoldoutConfig; -use ruvector_benchmarks::publishable_rvf::{generate_manifest, verify_manifest}; +use ruvector_benchmarks::publishable_rvf::{ + generate_manifest_with_rvf, verify_manifest, verify_rvf_binary, +}; #[derive(Parser)] #[command(name = "acceptance-rvf")] -#[command(about = "Publishable RVF acceptance test with witness chain verification")] +#[command(about = "Publishable RVF acceptance test with SHAKE-256 witness chain")] struct Cli { #[command(subcommand)] command: Commands, @@ -29,7 +35,7 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Generate a new acceptance test manifest + /// Generate a new acceptance test manifest (JSON + .rvf binary) Generate { /// Output JSON file path #[arg(short, long, default_value = "acceptance_manifest.json")] @@ -61,6 +67,12 @@ enum Commands { #[arg(short, long)] input: String, }, + /// Verify a native .rvf binary witness chain + VerifyRvf { + /// Input .rvf file path + #[arg(short, long)] + input: String, + }, } fn main() -> anyhow::Result<()> { @@ -86,17 +98,21 @@ fn main() -> anyhow::Result<()> { ..Default::default() }; + // Derive .rvf path from JSON output path + let rvf_path = output.replace(".json", ".rvf"); + println!("Generating acceptance test manifest..."); println!(" holdout={}, training={}, cycles={}, budget={}", holdout, training, cycles, budget); println!(); - let manifest = generate_manifest(&config)?; + let manifest = generate_manifest_with_rvf(&config, Some(&rvf_path))?; manifest.print_summary(); let json = serde_json::to_string_pretty(&manifest)?; std::fs::write(&output, &json)?; - println!(" Manifest written to: {}", output); + println!(" JSON manifest: {}", output); + println!(" RVF binary: {}", rvf_path); println!(" Chain root hash: {}", manifest.chain_root_hash); println!(); @@ -128,5 +144,18 @@ fn main() -> anyhow::Result<()> { std::process::exit(1); } } + Commands::VerifyRvf { input } => { + println!("Verifying .rvf witness chain: {}", input); + match verify_rvf_binary(&input) { + Ok(count) => { + println!(" WITNESS_SEG verified: {} entries, chain intact", count); + std::process::exit(0); + } + Err(e) => { + println!(" VERIFICATION FAILED: {}", e); + std::process::exit(1); + } + } + } } } diff --git a/examples/benchmarks/src/publishable_rvf.rs b/examples/benchmarks/src/publishable_rvf.rs index 1cb2cc1d8..f54805d8d 100644 --- a/examples/benchmarks/src/publishable_rvf.rs +++ b/examples/benchmarks/src/publishable_rvf.rs @@ -10,19 +10,24 @@ //! solve paths → identical outcomes. No network, no randomness, no clock. //! //! 2. **Witness chain**: Every puzzle decision (skip_mode chosen, context bucket, -//! steps taken, correct/wrong) is hashed into a SHA-256 chain. Changing any -//! single bit in any record invalidates the entire chain from that point. +//! steps taken, correct/wrong) is hashed into a SHAKE-256 chain using the +//! native `rvf-crypto` witness infrastructure. Changing any single bit in +//! any record invalidates the entire chain from that point. //! //! 3. **Graded scorecard**: Per-mode (A/B/C) aggregate metrics plus ablation //! assertions, all serialized to JSON. //! -//! 4. **Verification**: Re-run with same config → re-generate chain → compare +//! 4. **Binary .rvf output**: The witness chain is also written as a native +//! WITNESS_SEG (0x0A) + META_SEG (0x07) in the RVF wire format, producing +//! a `.rvf` file verifiable by any RVF-compatible tool or WASM runtime. +//! +//! 5. **Verification**: Re-run with same config → re-generate chain → compare //! chain root hash. If it matches, outcomes are identical. //! //! ## Usage //! //! ```bash -//! # Generate the manifest +//! # Generate the manifest (JSON + optional .rvf binary) //! cargo run --bin acceptance-rvf -- generate --output manifest.json //! //! # Verify a previously generated manifest @@ -33,8 +38,8 @@ use crate::acceptance_test::{ AblationMode, HoldoutConfig, run_acceptance_test_mode, }; use crate::temporal::PolicyKernel; +use rvf_crypto::shake256_256; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use std::collections::HashMap; // ═══════════════════════════════════════════════════════════════════════════ @@ -95,12 +100,12 @@ impl WitnessRecord { /// A witness record with its chain hash. /// -/// `chain_hash` = SHA-256(prev_chain_hash || canonical_bytes(record)) +/// `chain_hash` = SHAKE-256(prev_chain_hash || canonical_bytes(record)) /// First record: prev_chain_hash = [0; 32] (genesis) #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ChainedWitness { pub record: WitnessRecord, - /// Hex-encoded SHA-256 chain hash for this entry + /// Hex-encoded SHAKE-256 chain hash for this entry pub chain_hash: String, } @@ -181,7 +186,7 @@ pub struct RvfManifest { pub all_passed: bool, /// Witness chain (every puzzle decision, hash-linked) pub witness_chain: Vec, - /// SHA-256 of the final chain entry (hex). This is THE reproducibility proof. + /// SHAKE-256 of the final chain entry (hex). This is THE reproducibility proof. pub chain_root_hash: String, /// Total witness records in the chain pub chain_length: usize, @@ -218,9 +223,14 @@ impl From<&HoldoutConfig> for ManifestConfig { // Witness chain builder // ═══════════════════════════════════════════════════════════════════════════ -/// Builds a SHA-256-linked witness chain incrementally. +/// Builds a SHAKE-256-linked witness chain incrementally. +/// +/// Uses `rvf_crypto::shake256_256` for hashing, compatible with the +/// native RVF WITNESS_SEG format. pub struct WitnessChainBuilder { entries: Vec, + /// Parallel rvf-crypto WitnessEntry list for .rvf binary export + rvf_entries: Vec, prev_hash: [u8; 32], seq: usize, } @@ -229,6 +239,7 @@ impl WitnessChainBuilder { pub fn new() -> Self { Self { entries: Vec::new(), + rvf_entries: Vec::new(), prev_hash: [0u8; 32], seq: 0, } @@ -236,16 +247,29 @@ impl WitnessChainBuilder { /// Append a witness record to the chain. /// - /// The chain hash is: SHA-256(prev_hash || canonical_bytes(record)) + /// The chain hash is: SHAKE-256(prev_hash || canonical_bytes(record)) + /// Also builds the parallel rvf-crypto WitnessEntry for .rvf export. pub fn append(&mut self, mut record: WitnessRecord) { record.seq = self.seq; self.seq += 1; let canonical = record.canonical_bytes(); - let mut hasher = Sha256::new(); - hasher.update(&self.prev_hash); - hasher.update(&canonical); - let hash: [u8; 32] = hasher.finalize().into(); + // Compute the action_hash from canonical bytes using SHAKE-256 + let action_hash = shake256_256(&canonical); + + // Chain: SHAKE-256(prev_hash || canonical_bytes) + let mut chain_input = Vec::with_capacity(32 + canonical.len()); + chain_input.extend_from_slice(&self.prev_hash); + chain_input.extend_from_slice(&canonical); + let hash = shake256_256(&chain_input); + + // Build the rvf-crypto WitnessEntry (73-byte entry for .rvf binary) + self.rvf_entries.push(rvf_crypto::WitnessEntry { + prev_hash: [0u8; 32], // overwritten by create_witness_chain + action_hash, + timestamp_ns: self.seq as u64, // deterministic pseudo-timestamp + witness_type: 0x02, // COMPUTATION witness type + }); self.prev_hash = hash; self.entries.push(ChainedWitness { @@ -259,6 +283,11 @@ impl WitnessChainBuilder { let root = hex_encode(&self.prev_hash); (self.entries, root) } + + /// Get the rvf-crypto WitnessEntry list for .rvf binary export. + pub fn rvf_entries(&self) -> &[rvf_crypto::WitnessEntry] { + &self.rvf_entries + } } fn hex_encode(bytes: &[u8]) -> String { @@ -278,10 +307,10 @@ pub fn verify_chain(chain: &[ChainedWitness]) -> Result { for (i, entry) in chain.iter().enumerate() { let canonical = entry.record.canonical_bytes(); - let mut hasher = Sha256::new(); - hasher.update(&prev_hash); - hasher.update(&canonical); - let computed: [u8; 32] = hasher.finalize().into(); + let mut chain_input = Vec::with_capacity(32 + canonical.len()); + chain_input.extend_from_slice(&prev_hash); + chain_input.extend_from_slice(&canonical); + let computed = shake256_256(&chain_input); let computed_hex = hex_encode(&computed); if computed_hex != entry.chain_hash { @@ -300,7 +329,16 @@ pub fn verify_chain(chain: &[ChainedWitness]) -> Result { /// Run all three ablation modes and produce the publishable RVF manifest. /// /// This is the entry point. Same config → same manifest → same chain_root_hash. +/// If `rvf_output_path` is provided, also exports the native `.rvf` binary. pub fn generate_manifest(config: &HoldoutConfig) -> anyhow::Result { + generate_manifest_with_rvf(config, None) +} + +/// Like `generate_manifest`, but also produces a `.rvf` binary file. +pub fn generate_manifest_with_rvf( + config: &HoldoutConfig, + rvf_output_path: Option<&str>, +) -> anyhow::Result { let mut chain_builder = WitnessChainBuilder::new(); // Run all three modes @@ -332,14 +370,32 @@ pub fn generate_manifest(config: &HoldoutConfig) -> anyhow::Result && mode_b.result.passed && mode_c.result.passed; + // Export .rvf binary before finalizing (consumes chain_builder entries) + if let Some(rvf_path) = rvf_output_path { + // Build the manifest struct first for the meta segment + let preview_manifest = RvfManifest { + version: 2, + description: String::new(), + config: ManifestConfig::from(config), + scorecards: scorecards.clone(), + assertions: assertions.clone(), + all_passed, + witness_chain: vec![], + chain_root_hash: String::new(), + chain_length: chain_builder.rvf_entries().len(), + }; + export_rvf_binary(&preview_manifest, &chain_builder, rvf_path)?; + } + // Finalize witness chain let (witness_chain, chain_root_hash) = chain_builder.finalize(); let chain_length = witness_chain.len(); Ok(RvfManifest { - version: 1, + version: 2, description: "RuVector temporal reasoning ablation study — \ - deterministic acceptance test with SHA-256 witness chain" + deterministic acceptance test with SHAKE-256 witness chain \ + (rvf-crypto native)" .to_string(), config: ManifestConfig::from(config), scorecards, @@ -432,6 +488,106 @@ impl VerifyResult { } } +// ═══════════════════════════════════════════════════════════════════════════ +// Native .rvf binary export +// ═══════════════════════════════════════════════════════════════════════════ + +/// Export the manifest as a native `.rvf` binary file. +/// +/// Produces a file with two segments: +/// - **WITNESS_SEG** (0x0A): The SHAKE-256 witness chain as 73-byte entries +/// created by `rvf_crypto::create_witness_chain()`, verifiable by any +/// RVF-compatible tool including the WASM microkernel. +/// - **META_SEG** (0x07): JSON-encoded scorecard + assertions metadata. +/// +/// The resulting file is a valid `.rvf` file that can be inspected with +/// `rvf inspect`, verified with `rvf verify-witness`, or loaded in the +/// browser via the WASM runtime's `rvf_witness_verify` export. +pub fn export_rvf_binary( + manifest: &RvfManifest, + chain_builder: &WitnessChainBuilder, + path: &str, +) -> anyhow::Result<()> { + use rvf_crypto::create_witness_chain; + use rvf_types::{SegmentFlags, SegmentType}; + use rvf_wire::write_segment; + + // Build the native SHAKE-256 witness chain from the rvf-crypto entries + let witness_bytes = create_witness_chain(chain_builder.rvf_entries()); + + // Write WITNESS_SEG (0x0A) with SEALED flag + let witness_seg = write_segment( + SegmentType::Witness as u8, + &witness_bytes, + SegmentFlags::empty().with(SegmentFlags::SEALED), + 1, // segment_id + ); + + // Build metadata JSON payload + let meta = serde_json::json!({ + "format": "rvf-acceptance-test", + "version": manifest.version, + "chain_root_hash": manifest.chain_root_hash, + "chain_length": manifest.chain_length, + "all_passed": manifest.all_passed, + "config": manifest.config, + "scorecards": manifest.scorecards, + "assertions": manifest.assertions, + }); + let meta_bytes = serde_json::to_vec(&meta)?; + + // Write META_SEG (0x07) + let meta_seg = write_segment( + SegmentType::Meta as u8, + &meta_bytes, + SegmentFlags::empty().with(SegmentFlags::SEALED), + 2, // segment_id + ); + + // Concatenate segments into a single .rvf file + let mut rvf_file = Vec::with_capacity(witness_seg.len() + meta_seg.len()); + rvf_file.extend_from_slice(&witness_seg); + rvf_file.extend_from_slice(&meta_seg); + + std::fs::write(path, &rvf_file)?; + Ok(()) +} + +/// Verify the native `.rvf` binary witness chain. +/// +/// Reads the WITNESS_SEG payload and runs `rvf_crypto::verify_witness_chain`. +pub fn verify_rvf_binary(path: &str) -> anyhow::Result { + use rvf_crypto::verify_witness_chain; + use rvf_types::SEGMENT_HEADER_SIZE; + + let data = std::fs::read(path)?; + if data.len() < SEGMENT_HEADER_SIZE { + anyhow::bail!("File too small for a valid segment"); + } + + // Parse the first segment header to get payload length + let seg_type = data[5]; + if seg_type != 0x0A { + anyhow::bail!("First segment is not WITNESS_SEG (got 0x{:02X})", seg_type); + } + + let payload_len = u64::from_le_bytes( + data[0x10..0x18].try_into().map_err(|_| anyhow::anyhow!("Bad header"))? + ) as usize; + + let payload_start = SEGMENT_HEADER_SIZE; + let payload_end = payload_start + payload_len; + if data.len() < payload_end { + anyhow::bail!("Truncated witness payload"); + } + + let witness_data = &data[payload_start..payload_end]; + let entries = verify_witness_chain(witness_data) + .map_err(|e| anyhow::anyhow!("Witness chain verification failed: {:?}", e))?; + + Ok(entries.len()) +} + // ═══════════════════════════════════════════════════════════════════════════ // Internal helpers // ═══════════════════════════════════════════════════════════════════════════ @@ -787,7 +943,7 @@ mod tests { ..Default::default() }; let manifest = generate_manifest(&config).unwrap(); - assert_eq!(manifest.version, 1); + assert_eq!(manifest.version, 2); assert_eq!(manifest.scorecards.len(), 3); assert!(!manifest.chain_root_hash.is_empty()); assert!(manifest.chain_length > 0);