mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-29 19:33:34 +00:00
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:
parent
0dabec3e38
commit
aca7f6b197
13 changed files with 415 additions and 33 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -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
1
crates/rvf/Cargo.lock
generated
|
|
@ -1821,6 +1821,7 @@ name = "rvf-wasm"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dlmalloc",
|
||||
"rvf-crypto",
|
||||
"rvf-types",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =====================================================================
|
||||
|
|
|
|||
111
docs/adr/ADR-037-publishable-rvf-acceptance-test.md
Normal file
111
docs/adr/ADR-037-publishable-rvf-acceptance-test.md
Normal 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue