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
This commit is contained in:
Claude 2026-02-16 00:13:44 +00:00
parent 0dabec3e38
commit aca7f6b197
No known key found for this signature in database
13 changed files with 415 additions and 33 deletions

3
Cargo.lock generated
View file

@ -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",

1
crates/rvf/Cargo.lock generated
View file

@ -1821,6 +1821,7 @@ name = "rvf-wasm"
version = "0.1.0"
dependencies = [
"dlmalloc",
"rvf-crypto",
"rvf-types",
]

View file

@ -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 |

View file

@ -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"

View file

@ -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

View file

@ -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::{

View file

@ -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]

View file

@ -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 |

View file

@ -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
// =====================================================================

View file

@ -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

View file

@ -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"

View file

@ -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);
}
}
}
}
}

View file

@ -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<ChainedWitness>,
/// 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<ChainedWitness>,
/// Parallel rvf-crypto WitnessEntry list for .rvf binary export
rvf_entries: Vec<rvf_crypto::WitnessEntry>,
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<String, usize> {
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<String, usize> {
/// 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<RvfManifest> {
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<RvfManifest> {
let mut chain_builder = WitnessChainBuilder::new();
// Run all three modes
@ -332,14 +370,32 @@ pub fn generate_manifest(config: &HoldoutConfig) -> anyhow::Result<RvfManifest>
&& 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<usize> {
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);