Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
24
crates/rvf/rvf-types/Cargo.toml
Normal file
24
crates/rvf/rvf-types/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "rvf-types"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
description = "RuVector Format core types -- segment headers, enums, flags"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/ruvnet/ruvector"
|
||||
homepage = "https://github.com/ruvnet/ruvector"
|
||||
readme = "README.md"
|
||||
categories = ["data-structures", "no-std"]
|
||||
keywords = ["vector", "database", "binary-format", "simd"]
|
||||
rust-version = "1.87"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
alloc = []
|
||||
std = ["alloc"]
|
||||
serde = ["dep:serde"]
|
||||
ed25519 = ["dep:ed25519-dalek", "dep:rand_core"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
||||
ed25519-dalek = { version = "2", default-features = false, features = ["alloc", "rand_core"], optional = true }
|
||||
rand_core = { version = "0.6", default-features = false, optional = true }
|
||||
278
crates/rvf/rvf-types/README.md
Normal file
278
crates/rvf/rvf-types/README.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# rvf-types
|
||||
|
||||
Core type definitions for the RuVector Format (RVF) binary container.
|
||||
|
||||
## Overview
|
||||
|
||||
`rvf-types` defines the foundational types shared across all RVF crates:
|
||||
|
||||
- **Segment headers** -- magic bytes, version, flags, checksums
|
||||
- **Enums** -- element types (`F32`, `F16`, `U8`, `Binary`), compression modes, distance metrics
|
||||
- **Flags** -- segment-level feature flags (SIMD hints, encryption, quantization tier)
|
||||
|
||||
## Features
|
||||
|
||||
- `std` -- enable `std` support (disabled by default for `no_std` compatibility)
|
||||
- `serde` -- derive `Serialize`/`Deserialize` on all public types
|
||||
|
||||
## Usage
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rvf-types = "0.1"
|
||||
```
|
||||
|
||||
```rust
|
||||
use rvf_types::{SegmentHeader, ElementType, DistanceMetric};
|
||||
```
|
||||
|
||||
## Lineage / Derivation Types
|
||||
|
||||
`rvf-types` defines the types that power DNA-style lineage provenance for RVF files.
|
||||
|
||||
### `DerivationType` Enum
|
||||
|
||||
Describes how a child file was produced from its parent (`#[repr(u8)]`):
|
||||
|
||||
| Variant | Value | Meaning |
|
||||
|---------|-------|---------|
|
||||
| `Clone` | 0 | Exact copy of the parent |
|
||||
| `Filter` | 1 | Subset of parent data |
|
||||
| `Merge` | 2 | Multiple parents merged |
|
||||
| `Quantize` | 3 | Re-quantized from parent |
|
||||
| `Reindex` | 4 | HNSW rebuild or similar |
|
||||
| `Transform` | 5 | Arbitrary transformation |
|
||||
| `Snapshot` | 6 | Point-in-time snapshot |
|
||||
| `UserDefined` | 0xFF | Application-specific derivation |
|
||||
|
||||
### `FileIdentity` Struct (68 bytes, `repr(C)`)
|
||||
|
||||
Embedded in the Level0Root reserved area at offset `0xF00`. Old readers that ignore the reserved area see zeros and continue working.
|
||||
|
||||
| Offset | Size | Field |
|
||||
|--------|------|-------|
|
||||
| `0x00` | 16 | `file_id` -- UUID-style unique identifier |
|
||||
| `0x10` | 16 | `parent_id` -- parent file identifier (zeros for root) |
|
||||
| `0x20` | 32 | `parent_hash` -- SHAKE-256-256 of parent manifest (zeros for root) |
|
||||
| `0x40` | 4 | `lineage_depth` -- 0 for root, incremented per derivation |
|
||||
|
||||
```rust
|
||||
use rvf_types::FileIdentity;
|
||||
|
||||
let root = FileIdentity::new_root([0x42u8; 16]);
|
||||
assert!(root.is_root());
|
||||
assert_eq!(root.lineage_depth, 0);
|
||||
```
|
||||
|
||||
### `LineageRecord` Struct (128 bytes)
|
||||
|
||||
A fixed-size record for witness chain entries carrying full derivation metadata:
|
||||
|
||||
| Offset | Size | Field |
|
||||
|--------|------|-------|
|
||||
| `0x00` | 16 | `file_id` |
|
||||
| `0x10` | 16 | `parent_id` |
|
||||
| `0x20` | 32 | `parent_hash` |
|
||||
| `0x40` | 1 | `derivation_type` |
|
||||
| `0x41` | 3 | padding |
|
||||
| `0x44` | 4 | `mutation_count` |
|
||||
| `0x48` | 8 | `timestamp_ns` |
|
||||
| `0x50` | 1 | `description_len` |
|
||||
| `0x51` | 47 | `description` (UTF-8, max 47 bytes) |
|
||||
|
||||
### Lineage Witness Type Constants
|
||||
|
||||
These extend the witness chain event types for derivation tracking:
|
||||
|
||||
| Constant | Value | Purpose |
|
||||
|----------|-------|---------|
|
||||
| `WITNESS_DERIVATION` | `0x09` | File derivation event |
|
||||
| `WITNESS_LINEAGE_MERGE` | `0x0A` | Multi-parent merge |
|
||||
| `WITNESS_LINEAGE_SNAPSHOT` | `0x0B` | Snapshot event |
|
||||
| `WITNESS_LINEAGE_TRANSFORM` | `0x0C` | Transform event |
|
||||
| `WITNESS_LINEAGE_VERIFY` | `0x0D` | Lineage verification |
|
||||
|
||||
### `HAS_LINEAGE` Segment Flag
|
||||
|
||||
Bit 11 (`0x0800`) of the segment flags bitfield. Set when a file carries DNA-style lineage provenance metadata:
|
||||
|
||||
```rust
|
||||
use rvf_types::SegmentFlags;
|
||||
|
||||
let flags = SegmentFlags::empty().with(SegmentFlags::HAS_LINEAGE);
|
||||
assert!(flags.contains(SegmentFlags::HAS_LINEAGE));
|
||||
assert_eq!(flags.bits(), 0x0800);
|
||||
```
|
||||
|
||||
### Lineage Error Codes (Category `0x06`)
|
||||
|
||||
| Code | Name | Description |
|
||||
|------|------|-------------|
|
||||
| `0x0600` | `ParentNotFound` | Referenced parent file not found |
|
||||
| `0x0601` | `ParentHashMismatch` | Parent hash does not match recorded `parent_hash` |
|
||||
| `0x0602` | `LineageBroken` | Lineage chain has a missing link |
|
||||
| `0x0603` | `LineageCyclic` | Lineage chain contains a cycle |
|
||||
|
||||
## Computational Container Types
|
||||
|
||||
`rvf-types` defines the segment types and header structures for the RVF computational container model, which allows `.rvf` files to carry executable compute alongside vector data.
|
||||
|
||||
### Segment Types
|
||||
|
||||
Two segment type discriminants support the computational container:
|
||||
|
||||
| Variant | Value | Description |
|
||||
|---------|-------|-------------|
|
||||
| `SegmentType::Kernel` | `0x0E` | Embedded kernel / unikernel image for self-booting |
|
||||
| `SegmentType::Ebpf` | `0x0F` | Embedded eBPF program for kernel fast path |
|
||||
|
||||
These are defined in `segment_type.rs` and round-trip through `TryFrom<u8>`:
|
||||
|
||||
```rust
|
||||
use rvf_types::SegmentType;
|
||||
|
||||
assert_eq!(SegmentType::Kernel as u8, 0x0E);
|
||||
assert_eq!(SegmentType::Ebpf as u8, 0x0F);
|
||||
assert_eq!(SegmentType::try_from(0x0E), Ok(SegmentType::Kernel));
|
||||
```
|
||||
|
||||
### `KernelHeader` (128 bytes, `repr(C)`)
|
||||
|
||||
Describes an embedded unikernel or micro-Linux image within a KERNEL_SEG payload. Follows the standard 64-byte `SegmentHeader`.
|
||||
|
||||
| Offset | Size | Field | Description |
|
||||
|--------|------|-------|-------------|
|
||||
| `0x00` | 4 | `kernel_magic` | Magic: `0x52564B4E` ("RVKN") |
|
||||
| `0x04` | 2 | `header_version` | KernelHeader format version (currently 1) |
|
||||
| `0x06` | 1 | `arch` | Target architecture (`KernelArch` enum) |
|
||||
| `0x07` | 1 | `kernel_type` | Kernel type (`KernelType` enum) |
|
||||
| `0x08` | 4 | `kernel_flags` | Bitfield flags (`KernelFlags`) |
|
||||
| `0x0C` | 4 | `min_memory_mb` | Minimum RAM required (MiB) |
|
||||
| `0x10` | 8 | `entry_point` | Virtual address of kernel entry point |
|
||||
| `0x18` | 8 | `image_size` | Uncompressed kernel image size (bytes) |
|
||||
| `0x20` | 8 | `compressed_size` | Compressed kernel image size (bytes) |
|
||||
| `0x28` | 1 | `compression` | Compression algorithm (same as SegmentHeader) |
|
||||
| `0x29` | 1 | `api_transport` | API transport (`ApiTransport` enum) |
|
||||
| `0x2A` | 2 | `api_port` | Default API port (network byte order) |
|
||||
| `0x2C` | 4 | `api_version` | Supported RVF query API version |
|
||||
| `0x30` | 32 | `image_hash` | SHAKE-256-256 of uncompressed kernel image |
|
||||
| `0x50` | 16 | `build_id` | Unique build identifier (UUID v7) |
|
||||
| `0x60` | 8 | `build_timestamp` | Build time (nanosecond UNIX timestamp) |
|
||||
| `0x68` | 4 | `vcpu_count` | Recommended vCPU count (0 = single) |
|
||||
| `0x6C` | 4 | `reserved_0` | Reserved (must be zero) |
|
||||
| `0x70` | 8 | `cmdline_offset` | Offset to kernel command line within payload |
|
||||
| `0x78` | 4 | `cmdline_length` | Length of kernel command line (bytes) |
|
||||
| `0x7C` | 4 | `reserved_1` | Reserved (must be zero) |
|
||||
|
||||
### `EbpfHeader` (64 bytes, `repr(C)`)
|
||||
|
||||
Describes an embedded eBPF program within an EBPF_SEG payload.
|
||||
|
||||
| Offset | Size | Field | Description |
|
||||
|--------|------|-------|-------------|
|
||||
| `0x00` | 4 | `ebpf_magic` | Magic: `0x52564250` ("RVBP") |
|
||||
| `0x04` | 2 | `header_version` | EbpfHeader format version (currently 1) |
|
||||
| `0x06` | 1 | `program_type` | eBPF program type (`EbpfProgramType` enum) |
|
||||
| `0x07` | 1 | `attach_type` | eBPF attach point (`EbpfAttachType` enum) |
|
||||
| `0x08` | 4 | `program_flags` | Bitfield flags |
|
||||
| `0x0C` | 2 | `insn_count` | Number of BPF instructions (max 65535) |
|
||||
| `0x0E` | 2 | `max_dimension` | Maximum vector dimension this program handles |
|
||||
| `0x10` | 8 | `program_size` | ELF object size (bytes) |
|
||||
| `0x18` | 4 | `map_count` | Number of BPF maps defined |
|
||||
| `0x1C` | 4 | `btf_size` | BTF (BPF Type Format) section size |
|
||||
| `0x20` | 32 | `program_hash` | SHAKE-256-256 of the ELF object |
|
||||
|
||||
### Enums
|
||||
|
||||
#### `KernelArch` (`#[repr(u8)]`)
|
||||
|
||||
| Value | Name | Description |
|
||||
|-------|------|-------------|
|
||||
| `0x00` | `X86_64` | AMD64 / Intel 64 |
|
||||
| `0x01` | `Aarch64` | ARM 64-bit (ARMv8-A and later) |
|
||||
| `0x02` | `Riscv64` | RISC-V 64-bit (RV64GC) |
|
||||
| `0xFE` | `Universal` | Architecture-independent (e.g., interpreted) |
|
||||
| `0xFF` | `Unknown` | Reserved / unspecified |
|
||||
|
||||
#### `KernelType` (`#[repr(u8)]`)
|
||||
|
||||
| Value | Name | Description |
|
||||
|-------|------|-------------|
|
||||
| `0x00` | `Hermit` | Hermit OS unikernel (Rust-native) |
|
||||
| `0x01` | `MicroLinux` | Minimal Linux kernel (bzImage compatible) |
|
||||
| `0x02` | `Asterinas` | Asterinas framekernel (Linux ABI compatible) |
|
||||
| `0x03` | `WasiPreview2` | WASI Preview 2 component |
|
||||
| `0x04` | `Custom` | Custom kernel (requires external VMM knowledge) |
|
||||
| `0xFE` | `TestStub` | Test stub for CI (boots, reports health, exits) |
|
||||
| `0xFF` | `Reserved` | Reserved |
|
||||
|
||||
#### `ApiTransport` (`#[repr(u8)]`)
|
||||
|
||||
| Value | Name | Description |
|
||||
|-------|------|-------------|
|
||||
| `0x00` | `TcpHttp` | HTTP/1.1 over TCP (default) |
|
||||
| `0x01` | `TcpGrpc` | gRPC over TCP (HTTP/2) |
|
||||
| `0x02` | `Vsock` | VirtIO socket (Firecracker host-to-guest) |
|
||||
| `0x03` | `SharedMem` | Shared memory region (same-host co-location) |
|
||||
| `0xFF` | `None` | No network API (batch mode only) |
|
||||
|
||||
#### `EbpfProgramType` (`#[repr(u8)]`)
|
||||
|
||||
| Value | Name | Description |
|
||||
|-------|------|-------------|
|
||||
| `0x00` | `XdpDistance` | XDP program for distance computation on packets |
|
||||
| `0x01` | `TcFilter` | TC classifier for query routing |
|
||||
| `0x02` | `SocketFilter` | Socket filter for query preprocessing |
|
||||
| `0x03` | `Tracepoint` | Tracepoint for performance monitoring |
|
||||
| `0x04` | `Kprobe` | Kprobe for dynamic instrumentation |
|
||||
| `0x05` | `CgroupSkb` | Cgroup socket buffer filter |
|
||||
| `0xFF` | `Custom` | Custom program type |
|
||||
|
||||
#### `EbpfAttachType` (`#[repr(u8)]`)
|
||||
|
||||
| Value | Name | Description |
|
||||
|-------|------|-------------|
|
||||
| `0x00` | `XdpIngress` | XDP hook on NIC ingress |
|
||||
| `0x01` | `TcIngress` | TC ingress qdisc |
|
||||
| `0x02` | `TcEgress` | TC egress qdisc |
|
||||
| `0x03` | `SocketFilter` | Socket filter attachment |
|
||||
| `0x04` | `CgroupIngress` | Cgroup ingress |
|
||||
| `0x05` | `CgroupEgress` | Cgroup egress |
|
||||
| `0xFF` | `None` | No automatic attachment |
|
||||
|
||||
### `KernelFlags` Constants (u32 bitfield)
|
||||
|
||||
| Bit | Name | Description |
|
||||
|-----|------|-------------|
|
||||
| 0 | `REQUIRES_TEE` | Kernel must run inside a TEE enclave |
|
||||
| 1 | `REQUIRES_KVM` | Kernel requires KVM (hardware virtualization) |
|
||||
| 2 | `REQUIRES_UEFI` | Kernel requires UEFI boot |
|
||||
| 3 | `HAS_NETWORKING` | Kernel includes network stack |
|
||||
| 4 | `HAS_QUERY_API` | Kernel exposes RVF query API on `api_port` |
|
||||
| 5 | `HAS_INGEST_API` | Kernel exposes RVF ingest API |
|
||||
| 6 | `HAS_ADMIN_API` | Kernel exposes health/metrics API |
|
||||
| 7 | `ATTESTATION_READY` | Kernel can generate TEE attestation quotes |
|
||||
| 8 | `SIGNED` | Kernel image is signed (SignatureFooter follows) |
|
||||
| 9 | `MEASURED` | Kernel measurement stored in WITNESS_SEG |
|
||||
| 10 | `COMPRESSED` | Image is compressed (per compression field) |
|
||||
| 11 | `RELOCATABLE` | Kernel is position-independent |
|
||||
| 12 | `HAS_VIRTIO_NET` | Kernel includes VirtIO network driver |
|
||||
| 13 | `HAS_VIRTIO_BLK` | Kernel includes VirtIO block driver |
|
||||
| 14 | `HAS_VSOCK` | Kernel includes VSOCK for host communication |
|
||||
| 15-31 | reserved | Reserved (must be zero) |
|
||||
|
||||
### Three-Tier Execution Model
|
||||
|
||||
RVF supports a three-tier execution model where a single `.rvf` file can carry compute at multiple levels:
|
||||
|
||||
| Tier | Segment | Typical Size | Target Environment | Boot Time |
|
||||
|------|---------|-------------|--------------------|-----------|
|
||||
| **1: WASM** | WASM_SEG (existing) | 5.5 KB | Browser, edge, IoT | <1 ms |
|
||||
| **2: eBPF** | EBPF_SEG (`0x0F`) | 10-50 KB | Linux kernel fast path (XDP, TC) | <20 ms |
|
||||
| **3: Unikernel** | KERNEL_SEG (`0x0E`) | 200 KB - 2 MB | TEE enclaves, Firecracker, bare metal | <125 ms |
|
||||
|
||||
Files without KERNEL_SEG or EBPF_SEG continue to work unchanged. Readers that do not recognize these segment types skip them per the RVF forward-compatibility rule. See [ADR-030](../../../docs/adr/ADR-030-rvf-computational-container.md) for the full specification.
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
962
crates/rvf/rvf-types/src/agi_container.rs
Normal file
962
crates/rvf/rvf-types/src/agi_container.rs
Normal file
@@ -0,0 +1,962 @@
|
||||
//! AGI Cognitive Container types (ADR-036).
|
||||
//!
|
||||
//! An AGI container is a single RVF file that packages the complete intelligence
|
||||
//! runtime: micro Linux kernel, Claude Code orchestrator config, Claude Flow
|
||||
//! swarm manager, RuVector world model, evaluation harness, witness chains,
|
||||
//! and tool adapters.
|
||||
//!
|
||||
//! Wire format: 64-byte `AgiContainerHeader` + TLV manifest sections.
|
||||
//! The header is stored as a META segment (SegmentType::Meta) in the RVF file,
|
||||
//! alongside the KERNEL_SEG, WASM_SEG, VEC_SEG, INDEX_SEG, WITNESS_SEG, and
|
||||
//! CRYPTO_SEG that hold the actual payload data.
|
||||
|
||||
/// Magic bytes for AGI container manifest: "RVAG" (RuVector AGI).
|
||||
pub const AGI_MAGIC: u32 = 0x5256_4147;
|
||||
|
||||
/// Size of the AGI container header in bytes.
|
||||
pub const AGI_HEADER_SIZE: usize = 64;
|
||||
|
||||
/// Maximum container size: 16 GiB. Prevents unbounded resource consumption.
|
||||
pub const AGI_MAX_CONTAINER_SIZE: u64 = 16 * 1024 * 1024 * 1024;
|
||||
|
||||
// --- Flags ---
|
||||
|
||||
/// Container includes a KERNEL_SEG with micro Linux kernel.
|
||||
pub const AGI_HAS_KERNEL: u16 = 1 << 0;
|
||||
/// Container includes WASM_SEG modules.
|
||||
pub const AGI_HAS_WASM: u16 = 1 << 1;
|
||||
/// Container includes Claude Code + Claude Flow orchestrator config.
|
||||
pub const AGI_HAS_ORCHESTRATOR: u16 = 1 << 2;
|
||||
/// Container includes VEC_SEG + INDEX_SEG world model data.
|
||||
pub const AGI_HAS_WORLD_MODEL: u16 = 1 << 3;
|
||||
/// Container includes evaluation harness (task suite + graders).
|
||||
pub const AGI_HAS_EVAL: u16 = 1 << 4;
|
||||
/// Container includes promoted skill library.
|
||||
pub const AGI_HAS_SKILLS: u16 = 1 << 5;
|
||||
/// Container includes ADR-035 witness chain.
|
||||
pub const AGI_HAS_WITNESS: u16 = 1 << 6;
|
||||
/// Container is cryptographically signed (HMAC-SHA256 or Ed25519).
|
||||
pub const AGI_SIGNED: u16 = 1 << 7;
|
||||
/// All tool outputs stored — container supports replay mode.
|
||||
pub const AGI_REPLAY_CAPABLE: u16 = 1 << 8;
|
||||
/// Container can run without network (offline-first).
|
||||
pub const AGI_OFFLINE_CAPABLE: u16 = 1 << 9;
|
||||
/// Container includes MCP tool adapter registry.
|
||||
pub const AGI_HAS_TOOLS: u16 = 1 << 10;
|
||||
/// Container includes coherence gate configuration.
|
||||
pub const AGI_HAS_COHERENCE_GATES: u16 = 1 << 11;
|
||||
/// Container includes cross-domain transfer learning data.
|
||||
pub const AGI_HAS_DOMAIN_EXPANSION: u16 = 1 << 12;
|
||||
|
||||
// --- TLV tags for the manifest payload ---
|
||||
|
||||
/// Container UUID.
|
||||
pub const AGI_TAG_CONTAINER_ID: u16 = 0x0100;
|
||||
/// Build UUID.
|
||||
pub const AGI_TAG_BUILD_ID: u16 = 0x0101;
|
||||
/// Pinned model identifier (UTF-8 string, e.g. "claude-opus-4-6").
|
||||
pub const AGI_TAG_MODEL_ID: u16 = 0x0102;
|
||||
/// Serialized governance policy (binary, per ADR-035).
|
||||
pub const AGI_TAG_POLICY: u16 = 0x0103;
|
||||
/// Claude Code + Claude Flow orchestrator config (JSON or TOML).
|
||||
pub const AGI_TAG_ORCHESTRATOR: u16 = 0x0104;
|
||||
/// MCP tool adapter registry (JSON array of tool schemas).
|
||||
pub const AGI_TAG_TOOL_REGISTRY: u16 = 0x0105;
|
||||
/// Agent role prompts (one per agent type).
|
||||
pub const AGI_TAG_AGENT_PROMPTS: u16 = 0x0106;
|
||||
/// Evaluation task suite (JSON array of task specs).
|
||||
pub const AGI_TAG_EVAL_TASKS: u16 = 0x0107;
|
||||
/// Grading rules (JSON or binary grader config).
|
||||
pub const AGI_TAG_EVAL_GRADERS: u16 = 0x0108;
|
||||
/// Promoted skill library (serialized skill nodes).
|
||||
pub const AGI_TAG_SKILL_LIBRARY: u16 = 0x0109;
|
||||
/// Replay automation script.
|
||||
pub const AGI_TAG_REPLAY_SCRIPT: u16 = 0x010A;
|
||||
/// Kernel boot parameters (command line, initrd config).
|
||||
pub const AGI_TAG_KERNEL_CONFIG: u16 = 0x010B;
|
||||
/// Network configuration (ports, endpoints, TLS).
|
||||
pub const AGI_TAG_NETWORK_CONFIG: u16 = 0x010C;
|
||||
/// Coherence gate thresholds and rules.
|
||||
pub const AGI_TAG_COHERENCE_CONFIG: u16 = 0x010D;
|
||||
/// Claude.md project instructions.
|
||||
pub const AGI_TAG_PROJECT_INSTRUCTIONS: u16 = 0x010E;
|
||||
/// Dependency snapshot hashes (pinned repos, packages).
|
||||
pub const AGI_TAG_DEPENDENCY_SNAPSHOT: u16 = 0x010F;
|
||||
/// Authority level and resource budget configuration.
|
||||
pub const AGI_TAG_AUTHORITY_CONFIG: u16 = 0x0110;
|
||||
/// Target domain profile identifier.
|
||||
pub const AGI_TAG_DOMAIN_PROFILE: u16 = 0x0111;
|
||||
/// Cross-domain transfer prior (posterior summaries).
|
||||
pub const AGI_TAG_TRANSFER_PRIOR: u16 = 0x0112;
|
||||
/// Policy kernel configuration and performance history.
|
||||
pub const AGI_TAG_POLICY_KERNEL: u16 = 0x0113;
|
||||
/// Cost curve convergence and acceleration data.
|
||||
pub const AGI_TAG_COST_CURVE: u16 = 0x0114;
|
||||
/// Counterexample archive (failed solutions for future decisions).
|
||||
pub const AGI_TAG_COUNTEREXAMPLES: u16 = 0x0115;
|
||||
|
||||
// --- Execution mode ---
|
||||
|
||||
/// Container execution mode.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum ExecutionMode {
|
||||
/// Replay: no external tool calls, use stored receipts.
|
||||
/// All graders must match exactly. Witness chain must match.
|
||||
Replay = 0,
|
||||
/// Verify: live tool calls, outputs stored and hashed.
|
||||
/// Outputs must pass same tests. Costs within expected bounds.
|
||||
Verify = 1,
|
||||
/// Live: full autonomous operation with governance controls.
|
||||
Live = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ExecutionMode {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Replay),
|
||||
1 => Ok(Self::Verify),
|
||||
2 => Ok(Self::Live),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Authority level ---
|
||||
|
||||
/// Authority level controlling what actions a container execution can perform.
|
||||
///
|
||||
/// Each action in the world model must reference a policy decision node
|
||||
/// that grants at least the required authority level.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum AuthorityLevel {
|
||||
/// Read-only: query vectors, graphs, memories. No mutations.
|
||||
ReadOnly = 0,
|
||||
/// Write to internal memory: commit world model deltas behind
|
||||
/// coherence gates. No external tool calls.
|
||||
WriteMemory = 1,
|
||||
/// Execute tools: run sandboxed tools (file read/write, tests,
|
||||
/// code generation). External side effects gated by policy.
|
||||
ExecuteTools = 2,
|
||||
/// Write external: push code, create PRs, send messages, modify
|
||||
/// infrastructure. Requires explicit policy grant per action class.
|
||||
WriteExternal = 3,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for AuthorityLevel {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::ReadOnly),
|
||||
1 => Ok(Self::WriteMemory),
|
||||
2 => Ok(Self::ExecuteTools),
|
||||
3 => Ok(Self::WriteExternal),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthorityLevel {
|
||||
/// Default authority for the given execution mode.
|
||||
pub const fn default_for_mode(mode: ExecutionMode) -> Self {
|
||||
match mode {
|
||||
ExecutionMode::Replay => Self::ReadOnly,
|
||||
ExecutionMode::Verify => Self::ExecuteTools,
|
||||
ExecutionMode::Live => Self::WriteMemory,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this authority level permits a given required level.
|
||||
pub const fn permits(&self, required: AuthorityLevel) -> bool {
|
||||
(*self as u8) >= (required as u8)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Resource budgets ---
|
||||
|
||||
/// Per-task resource budget with hard caps.
|
||||
///
|
||||
/// Budget exhaustion triggers graceful degradation: the task enters `Skipped`
|
||||
/// outcome with a `BudgetExhausted` postmortem in the witness bundle.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ResourceBudget {
|
||||
/// Maximum wall-clock time per task in seconds. Default: 300.
|
||||
pub max_time_secs: u32,
|
||||
/// Maximum total model tokens per task. Default: 200,000.
|
||||
pub max_tokens: u32,
|
||||
/// Maximum cost per task in microdollars. Default: 1,000,000 ($1.00).
|
||||
pub max_cost_microdollars: u32,
|
||||
/// Maximum tool calls per task. Default: 50.
|
||||
pub max_tool_calls: u16,
|
||||
/// Maximum external write actions per task. Default: 0.
|
||||
pub max_external_writes: u16,
|
||||
}
|
||||
|
||||
impl Default for ResourceBudget {
|
||||
fn default() -> Self {
|
||||
Self::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceBudget {
|
||||
/// Default resource budget for a single task.
|
||||
pub const DEFAULT: Self = Self {
|
||||
max_time_secs: 300,
|
||||
max_tokens: 200_000,
|
||||
max_cost_microdollars: 1_000_000,
|
||||
max_tool_calls: 50,
|
||||
max_external_writes: 0,
|
||||
};
|
||||
|
||||
/// Extended budget (4x default) for high-value tasks.
|
||||
pub const EXTENDED: Self = Self {
|
||||
max_time_secs: 1200,
|
||||
max_tokens: 800_000,
|
||||
max_cost_microdollars: 4_000_000,
|
||||
max_tool_calls: 200,
|
||||
max_external_writes: 10,
|
||||
};
|
||||
|
||||
/// Maximum configurable budget (hard ceiling, not overridable).
|
||||
pub const MAX: Self = Self {
|
||||
max_time_secs: 3600,
|
||||
max_tokens: 1_000_000,
|
||||
max_cost_microdollars: 10_000_000,
|
||||
max_tool_calls: 500,
|
||||
max_external_writes: 50,
|
||||
};
|
||||
|
||||
/// Clamp this budget to not exceed the MAX limits.
|
||||
pub const fn clamped(self) -> Self {
|
||||
Self {
|
||||
max_time_secs: if self.max_time_secs > Self::MAX.max_time_secs {
|
||||
Self::MAX.max_time_secs
|
||||
} else {
|
||||
self.max_time_secs
|
||||
},
|
||||
max_tokens: if self.max_tokens > Self::MAX.max_tokens {
|
||||
Self::MAX.max_tokens
|
||||
} else {
|
||||
self.max_tokens
|
||||
},
|
||||
max_cost_microdollars: if self.max_cost_microdollars > Self::MAX.max_cost_microdollars {
|
||||
Self::MAX.max_cost_microdollars
|
||||
} else {
|
||||
self.max_cost_microdollars
|
||||
},
|
||||
max_tool_calls: if self.max_tool_calls > Self::MAX.max_tool_calls {
|
||||
Self::MAX.max_tool_calls
|
||||
} else {
|
||||
self.max_tool_calls
|
||||
},
|
||||
max_external_writes: if self.max_external_writes > Self::MAX.max_external_writes {
|
||||
Self::MAX.max_external_writes
|
||||
} else {
|
||||
self.max_external_writes
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Coherence thresholds ---
|
||||
|
||||
/// Configurable coherence thresholds for structural health gating.
|
||||
///
|
||||
/// These map to ADR-033's quality framework: the coherence score is analogous
|
||||
/// to `ResponseQuality` -- it signals whether the system's internal state is
|
||||
/// trustworthy enough to act on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct CoherenceThresholds {
|
||||
/// Minimum coherence score (0.0 to 1.0). Below this, block all commits
|
||||
/// and enter repair mode. Default: 0.70.
|
||||
pub min_coherence_score: f32,
|
||||
/// Maximum contradiction rate (contradictions per 100 events).
|
||||
/// Above this, freeze skill promotion. Default: 5.0.
|
||||
pub max_contradiction_rate: f32,
|
||||
/// Maximum rollback ratio (fraction of tasks that required rollback).
|
||||
/// Above this, halt Live execution; require human review. Default: 0.20.
|
||||
pub max_rollback_ratio: f32,
|
||||
}
|
||||
|
||||
impl Default for CoherenceThresholds {
|
||||
fn default() -> Self {
|
||||
Self::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
impl CoherenceThresholds {
|
||||
/// Default coherence thresholds.
|
||||
pub const DEFAULT: Self = Self {
|
||||
min_coherence_score: 0.70,
|
||||
max_contradiction_rate: 5.0,
|
||||
max_rollback_ratio: 0.20,
|
||||
};
|
||||
|
||||
/// Strict thresholds for production.
|
||||
pub const STRICT: Self = Self {
|
||||
min_coherence_score: 0.85,
|
||||
max_contradiction_rate: 2.0,
|
||||
max_rollback_ratio: 0.10,
|
||||
};
|
||||
|
||||
/// Validate that threshold values are within valid ranges.
|
||||
pub fn validate(&self) -> Result<(), ContainerError> {
|
||||
if self.min_coherence_score < 0.0 || self.min_coherence_score > 1.0 {
|
||||
return Err(ContainerError::InvalidConfig(
|
||||
"min_coherence_score must be in [0.0, 1.0]",
|
||||
));
|
||||
}
|
||||
if self.max_contradiction_rate < 0.0 {
|
||||
return Err(ContainerError::InvalidConfig(
|
||||
"max_contradiction_rate must be >= 0.0",
|
||||
));
|
||||
}
|
||||
if self.max_rollback_ratio < 0.0 || self.max_rollback_ratio > 1.0 {
|
||||
return Err(ContainerError::InvalidConfig(
|
||||
"max_rollback_ratio must be in [0.0, 1.0]",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire-format AGI container header (exactly 64 bytes, `repr(C)`).
|
||||
///
|
||||
/// ```text
|
||||
/// Offset Type Field
|
||||
/// 0x00 u32 magic (0x52564147 "RVAG")
|
||||
/// 0x04 u16 version
|
||||
/// 0x06 u16 flags
|
||||
/// 0x08 [u8; 16] container_id (UUID)
|
||||
/// 0x18 [u8; 16] build_id (UUID)
|
||||
/// 0x28 u64 created_ns (UNIX epoch nanoseconds)
|
||||
/// 0x30 [u8; 8] model_id_hash (SHA-256 truncated)
|
||||
/// 0x38 [u8; 8] policy_hash (SHA-256 truncated)
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct AgiContainerHeader {
|
||||
/// Magic bytes: AGI_MAGIC.
|
||||
pub magic: u32,
|
||||
/// Format version (currently 1).
|
||||
pub version: u16,
|
||||
/// Bitfield flags indicating which segments are present.
|
||||
pub flags: u16,
|
||||
/// Unique container identifier (UUID).
|
||||
pub container_id: [u8; 16],
|
||||
/// Build identifier (UUID, changes on each repackaging).
|
||||
pub build_id: [u8; 16],
|
||||
/// Creation timestamp (nanoseconds since UNIX epoch).
|
||||
pub created_ns: u64,
|
||||
/// SHA-256 of the pinned model identifier, truncated to 8 bytes.
|
||||
pub model_id_hash: [u8; 8],
|
||||
/// SHA-256 of the governance policy, truncated to 8 bytes.
|
||||
pub policy_hash: [u8; 8],
|
||||
}
|
||||
|
||||
// Compile-time size assertion.
|
||||
const _: () = assert!(core::mem::size_of::<AgiContainerHeader>() == 64);
|
||||
|
||||
impl AgiContainerHeader {
|
||||
/// Check magic bytes.
|
||||
pub const fn is_valid_magic(&self) -> bool {
|
||||
self.magic == AGI_MAGIC
|
||||
}
|
||||
|
||||
/// Check if the container is signed.
|
||||
pub const fn is_signed(&self) -> bool {
|
||||
self.flags & AGI_SIGNED != 0
|
||||
}
|
||||
|
||||
/// Check if the container has a micro Linux kernel.
|
||||
pub const fn has_kernel(&self) -> bool {
|
||||
self.flags & AGI_HAS_KERNEL != 0
|
||||
}
|
||||
|
||||
/// Check if the container has an orchestrator config.
|
||||
pub const fn has_orchestrator(&self) -> bool {
|
||||
self.flags & AGI_HAS_ORCHESTRATOR != 0
|
||||
}
|
||||
|
||||
/// Check if the container supports replay mode.
|
||||
pub const fn is_replay_capable(&self) -> bool {
|
||||
self.flags & AGI_REPLAY_CAPABLE != 0
|
||||
}
|
||||
|
||||
/// Check if the container can run offline.
|
||||
pub const fn is_offline_capable(&self) -> bool {
|
||||
self.flags & AGI_OFFLINE_CAPABLE != 0
|
||||
}
|
||||
|
||||
/// Check if the container has a world model (VEC + INDEX segments).
|
||||
pub const fn has_world_model(&self) -> bool {
|
||||
self.flags & AGI_HAS_WORLD_MODEL != 0
|
||||
}
|
||||
|
||||
/// Check if the container has coherence gate configuration.
|
||||
pub const fn has_coherence_gates(&self) -> bool {
|
||||
self.flags & AGI_HAS_COHERENCE_GATES != 0
|
||||
}
|
||||
|
||||
/// Check if the container has domain expansion data.
|
||||
pub const fn has_domain_expansion(&self) -> bool {
|
||||
self.flags & AGI_HAS_DOMAIN_EXPANSION != 0
|
||||
}
|
||||
|
||||
/// Serialize header to a 64-byte array.
|
||||
pub fn to_bytes(&self) -> [u8; AGI_HEADER_SIZE] {
|
||||
let mut buf = [0u8; AGI_HEADER_SIZE];
|
||||
buf[0..4].copy_from_slice(&self.magic.to_le_bytes());
|
||||
buf[4..6].copy_from_slice(&self.version.to_le_bytes());
|
||||
buf[6..8].copy_from_slice(&self.flags.to_le_bytes());
|
||||
buf[8..24].copy_from_slice(&self.container_id);
|
||||
buf[24..40].copy_from_slice(&self.build_id);
|
||||
buf[40..48].copy_from_slice(&self.created_ns.to_le_bytes());
|
||||
buf[48..56].copy_from_slice(&self.model_id_hash);
|
||||
buf[56..64].copy_from_slice(&self.policy_hash);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize header from a byte slice (>= 64 bytes).
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, crate::RvfError> {
|
||||
if data.len() < AGI_HEADER_SIZE {
|
||||
return Err(crate::RvfError::SizeMismatch {
|
||||
expected: AGI_HEADER_SIZE,
|
||||
got: data.len(),
|
||||
});
|
||||
}
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != AGI_MAGIC {
|
||||
return Err(crate::RvfError::BadMagic {
|
||||
expected: AGI_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
let mut container_id = [0u8; 16];
|
||||
container_id.copy_from_slice(&data[8..24]);
|
||||
let mut build_id = [0u8; 16];
|
||||
build_id.copy_from_slice(&data[24..40]);
|
||||
let mut model_id_hash = [0u8; 8];
|
||||
model_id_hash.copy_from_slice(&data[48..56]);
|
||||
let mut policy_hash = [0u8; 8];
|
||||
policy_hash.copy_from_slice(&data[56..64]);
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version: u16::from_le_bytes([data[4], data[5]]),
|
||||
flags: u16::from_le_bytes([data[6], data[7]]),
|
||||
container_id,
|
||||
build_id,
|
||||
created_ns: u64::from_le_bytes([
|
||||
data[40], data[41], data[42], data[43], data[44], data[45], data[46], data[47],
|
||||
]),
|
||||
model_id_hash,
|
||||
policy_hash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Required segments for a valid AGI container.
|
||||
///
|
||||
/// Used by the container builder/validator to ensure completeness.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ContainerSegments {
|
||||
/// KERNEL_SEG: micro Linux kernel (e.g. Firecracker-compatible vmlinux).
|
||||
pub kernel_present: bool,
|
||||
/// KERNEL_SEG size in bytes.
|
||||
pub kernel_size: u64,
|
||||
/// WASM_SEG: interpreter + microkernel modules.
|
||||
pub wasm_count: u16,
|
||||
/// Total WASM_SEG size in bytes.
|
||||
pub wasm_total_size: u64,
|
||||
/// VEC_SEG: world model vector count.
|
||||
pub vec_segment_count: u16,
|
||||
/// INDEX_SEG: HNSW index count.
|
||||
pub index_segment_count: u16,
|
||||
/// WITNESS_SEG: witness bundle count.
|
||||
pub witness_count: u32,
|
||||
/// CRYPTO_SEG: present.
|
||||
pub crypto_present: bool,
|
||||
/// META segment with AGI manifest: present.
|
||||
pub manifest_present: bool,
|
||||
/// Orchestrator configuration present.
|
||||
pub orchestrator_present: bool,
|
||||
/// World model data present (VEC + INDEX segments).
|
||||
pub world_model_present: bool,
|
||||
/// Domain expansion (transfer priors, policy kernels, cost curves) present.
|
||||
pub domain_expansion_present: bool,
|
||||
/// Total container size in bytes.
|
||||
pub total_size: u64,
|
||||
}
|
||||
|
||||
impl ContainerSegments {
|
||||
/// Validate that the container has all required segments for a given
|
||||
/// execution mode.
|
||||
pub fn validate(&self, mode: ExecutionMode) -> Result<(), ContainerError> {
|
||||
// All modes require the manifest.
|
||||
if !self.manifest_present {
|
||||
return Err(ContainerError::MissingSegment("AGI manifest"));
|
||||
}
|
||||
|
||||
// Size check.
|
||||
if self.total_size > AGI_MAX_CONTAINER_SIZE {
|
||||
return Err(ContainerError::TooLarge {
|
||||
size: self.total_size,
|
||||
});
|
||||
}
|
||||
|
||||
match mode {
|
||||
ExecutionMode::Replay => {
|
||||
// Replay needs witness chains.
|
||||
if self.witness_count == 0 {
|
||||
return Err(ContainerError::MissingSegment("witness chain"));
|
||||
}
|
||||
}
|
||||
ExecutionMode::Verify | ExecutionMode::Live => {
|
||||
// Verify/Live need at least kernel or WASM.
|
||||
if !self.kernel_present && self.wasm_count == 0 {
|
||||
return Err(ContainerError::MissingSegment("kernel or WASM runtime"));
|
||||
}
|
||||
// Verify/Live need world model data for meaningful operation.
|
||||
if !self.world_model_present
|
||||
&& self.vec_segment_count == 0
|
||||
&& self.index_segment_count == 0
|
||||
{
|
||||
return Err(ContainerError::MissingSegment(
|
||||
"world model (VEC or INDEX segments)",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the flags bitfield from present segments.
|
||||
pub fn to_flags(&self) -> u16 {
|
||||
let mut flags: u16 = 0;
|
||||
if self.kernel_present {
|
||||
flags |= AGI_HAS_KERNEL;
|
||||
}
|
||||
if self.wasm_count > 0 {
|
||||
flags |= AGI_HAS_WASM;
|
||||
}
|
||||
if self.witness_count > 0 {
|
||||
flags |= AGI_HAS_WITNESS;
|
||||
}
|
||||
if self.crypto_present {
|
||||
flags |= AGI_SIGNED;
|
||||
}
|
||||
if self.orchestrator_present {
|
||||
flags |= AGI_HAS_ORCHESTRATOR;
|
||||
}
|
||||
if self.world_model_present || self.vec_segment_count > 0 || self.index_segment_count > 0 {
|
||||
flags |= AGI_HAS_WORLD_MODEL;
|
||||
}
|
||||
if self.domain_expansion_present {
|
||||
flags |= AGI_HAS_DOMAIN_EXPANSION;
|
||||
}
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for AGI container operations.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ContainerError {
|
||||
/// A required segment is missing.
|
||||
MissingSegment(&'static str),
|
||||
/// Container exceeds size limit.
|
||||
TooLarge { size: u64 },
|
||||
/// Invalid segment configuration.
|
||||
InvalidConfig(&'static str),
|
||||
/// Signature verification failed.
|
||||
SignatureInvalid,
|
||||
/// Authority level insufficient for the requested action.
|
||||
InsufficientAuthority { required: u8, granted: u8 },
|
||||
/// Resource budget exceeded.
|
||||
BudgetExhausted(&'static str),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ContainerError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
ContainerError::MissingSegment(s) => write!(f, "missing segment: {s}"),
|
||||
ContainerError::TooLarge { size } => {
|
||||
write!(f, "container too large: {size} bytes")
|
||||
}
|
||||
ContainerError::InvalidConfig(s) => write!(f, "invalid config: {s}"),
|
||||
ContainerError::SignatureInvalid => {
|
||||
write!(f, "signature verification failed")
|
||||
}
|
||||
ContainerError::InsufficientAuthority { required, granted } => {
|
||||
write!(
|
||||
f,
|
||||
"insufficient authority: required level {required}, granted {granted}"
|
||||
)
|
||||
}
|
||||
ContainerError::BudgetExhausted(resource) => {
|
||||
write!(f, "resource budget exhausted: {resource}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::format;
|
||||
|
||||
#[test]
|
||||
fn agi_header_size() {
|
||||
assert_eq!(core::mem::size_of::<AgiContainerHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agi_header_round_trip() {
|
||||
let hdr = AgiContainerHeader {
|
||||
magic: AGI_MAGIC,
|
||||
version: 1,
|
||||
flags: AGI_HAS_KERNEL
|
||||
| AGI_HAS_ORCHESTRATOR
|
||||
| AGI_HAS_WORLD_MODEL
|
||||
| AGI_HAS_EVAL
|
||||
| AGI_SIGNED
|
||||
| AGI_REPLAY_CAPABLE,
|
||||
container_id: [0x42; 16],
|
||||
build_id: [0x43; 16],
|
||||
created_ns: 1_700_000_000_000_000_000,
|
||||
model_id_hash: [0xAA; 8],
|
||||
policy_hash: [0xBB; 8],
|
||||
};
|
||||
let bytes = hdr.to_bytes();
|
||||
assert_eq!(bytes.len(), AGI_HEADER_SIZE);
|
||||
let decoded = AgiContainerHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded, hdr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agi_header_bad_magic() {
|
||||
let mut bytes = [0u8; 64];
|
||||
bytes[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
|
||||
assert!(AgiContainerHeader::from_bytes(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agi_header_too_short() {
|
||||
assert!(AgiContainerHeader::from_bytes(&[0u8; 32]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agi_flags() {
|
||||
let hdr = AgiContainerHeader {
|
||||
magic: AGI_MAGIC,
|
||||
version: 1,
|
||||
flags: AGI_HAS_KERNEL | AGI_HAS_ORCHESTRATOR | AGI_SIGNED,
|
||||
container_id: [0; 16],
|
||||
build_id: [0; 16],
|
||||
created_ns: 0,
|
||||
model_id_hash: [0; 8],
|
||||
policy_hash: [0; 8],
|
||||
};
|
||||
assert!(hdr.has_kernel());
|
||||
assert!(hdr.has_orchestrator());
|
||||
assert!(hdr.is_signed());
|
||||
assert!(!hdr.is_replay_capable());
|
||||
assert!(!hdr.is_offline_capable());
|
||||
assert!(!hdr.has_world_model());
|
||||
assert!(!hdr.has_coherence_gates());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_mode_round_trip() {
|
||||
for raw in 0..=2u8 {
|
||||
let m = ExecutionMode::try_from(raw).unwrap();
|
||||
assert_eq!(m as u8, raw);
|
||||
}
|
||||
assert!(ExecutionMode::try_from(3).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_validate_replay_needs_witness() {
|
||||
let segs = ContainerSegments {
|
||||
manifest_present: true,
|
||||
witness_count: 0,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
segs.validate(ExecutionMode::Replay),
|
||||
Err(ContainerError::MissingSegment("witness chain"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_validate_live_needs_runtime() {
|
||||
let segs = ContainerSegments {
|
||||
manifest_present: true,
|
||||
kernel_present: false,
|
||||
wasm_count: 0,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
segs.validate(ExecutionMode::Live),
|
||||
Err(ContainerError::MissingSegment("kernel or WASM runtime"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_validate_live_needs_world_model() {
|
||||
let segs = ContainerSegments {
|
||||
manifest_present: true,
|
||||
kernel_present: true,
|
||||
vec_segment_count: 0,
|
||||
index_segment_count: 0,
|
||||
world_model_present: false,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
segs.validate(ExecutionMode::Live),
|
||||
Err(ContainerError::MissingSegment(
|
||||
"world model (VEC or INDEX segments)"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_validate_live_with_kernel_and_world_model() {
|
||||
let segs = ContainerSegments {
|
||||
manifest_present: true,
|
||||
kernel_present: true,
|
||||
world_model_present: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(segs.validate(ExecutionMode::Live).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_validate_live_with_wasm_and_vec() {
|
||||
let segs = ContainerSegments {
|
||||
manifest_present: true,
|
||||
wasm_count: 2,
|
||||
vec_segment_count: 1,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(segs.validate(ExecutionMode::Live).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_validate_replay_with_witness() {
|
||||
let segs = ContainerSegments {
|
||||
manifest_present: true,
|
||||
witness_count: 10,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(segs.validate(ExecutionMode::Replay).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_validate_too_large() {
|
||||
let segs = ContainerSegments {
|
||||
manifest_present: true,
|
||||
total_size: AGI_MAX_CONTAINER_SIZE + 1,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
segs.validate(ExecutionMode::Replay),
|
||||
Err(ContainerError::TooLarge {
|
||||
size: AGI_MAX_CONTAINER_SIZE + 1
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_to_flags() {
|
||||
let segs = ContainerSegments {
|
||||
kernel_present: true,
|
||||
wasm_count: 1,
|
||||
witness_count: 5,
|
||||
crypto_present: true,
|
||||
orchestrator_present: true,
|
||||
vec_segment_count: 3,
|
||||
..Default::default()
|
||||
};
|
||||
let flags = segs.to_flags();
|
||||
assert_ne!(flags & AGI_HAS_KERNEL, 0);
|
||||
assert_ne!(flags & AGI_HAS_WASM, 0);
|
||||
assert_ne!(flags & AGI_HAS_WITNESS, 0);
|
||||
assert_ne!(flags & AGI_SIGNED, 0);
|
||||
assert_ne!(flags & AGI_HAS_ORCHESTRATOR, 0);
|
||||
assert_ne!(flags & AGI_HAS_WORLD_MODEL, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn container_error_display() {
|
||||
let e = ContainerError::MissingSegment("kernel");
|
||||
assert!(format!("{e}").contains("kernel"));
|
||||
let e2 = ContainerError::TooLarge { size: 999 };
|
||||
assert!(format!("{e2}").contains("999"));
|
||||
let e3 = ContainerError::InsufficientAuthority {
|
||||
required: 3,
|
||||
granted: 1,
|
||||
};
|
||||
assert!(format!("{e3}").contains("required level 3"));
|
||||
let e4 = ContainerError::BudgetExhausted("tokens");
|
||||
assert!(format!("{e4}").contains("tokens"));
|
||||
}
|
||||
|
||||
// --- Authority level tests ---
|
||||
|
||||
#[test]
|
||||
fn authority_level_round_trip() {
|
||||
for raw in 0..=3u8 {
|
||||
let a = AuthorityLevel::try_from(raw).unwrap();
|
||||
assert_eq!(a as u8, raw);
|
||||
}
|
||||
assert!(AuthorityLevel::try_from(4).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_level_ordering() {
|
||||
assert!(AuthorityLevel::ReadOnly < AuthorityLevel::WriteMemory);
|
||||
assert!(AuthorityLevel::WriteMemory < AuthorityLevel::ExecuteTools);
|
||||
assert!(AuthorityLevel::ExecuteTools < AuthorityLevel::WriteExternal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_permits() {
|
||||
assert!(AuthorityLevel::WriteExternal.permits(AuthorityLevel::ReadOnly));
|
||||
assert!(AuthorityLevel::WriteExternal.permits(AuthorityLevel::WriteExternal));
|
||||
assert!(AuthorityLevel::ExecuteTools.permits(AuthorityLevel::WriteMemory));
|
||||
assert!(!AuthorityLevel::ReadOnly.permits(AuthorityLevel::WriteMemory));
|
||||
assert!(!AuthorityLevel::WriteMemory.permits(AuthorityLevel::ExecuteTools));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_default_for_mode() {
|
||||
assert_eq!(
|
||||
AuthorityLevel::default_for_mode(ExecutionMode::Replay),
|
||||
AuthorityLevel::ReadOnly
|
||||
);
|
||||
assert_eq!(
|
||||
AuthorityLevel::default_for_mode(ExecutionMode::Verify),
|
||||
AuthorityLevel::ExecuteTools
|
||||
);
|
||||
assert_eq!(
|
||||
AuthorityLevel::default_for_mode(ExecutionMode::Live),
|
||||
AuthorityLevel::WriteMemory
|
||||
);
|
||||
}
|
||||
|
||||
// --- Resource budget tests ---
|
||||
|
||||
#[test]
|
||||
fn resource_budget_default() {
|
||||
let b = ResourceBudget::default();
|
||||
assert_eq!(b.max_time_secs, 300);
|
||||
assert_eq!(b.max_tokens, 200_000);
|
||||
assert_eq!(b.max_cost_microdollars, 1_000_000);
|
||||
assert_eq!(b.max_tool_calls, 50);
|
||||
assert_eq!(b.max_external_writes, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_budget_clamped() {
|
||||
let over = ResourceBudget {
|
||||
max_time_secs: 999_999,
|
||||
max_tokens: 999_999_999,
|
||||
max_cost_microdollars: 999_999_999,
|
||||
max_tool_calls: 60_000,
|
||||
max_external_writes: 60_000,
|
||||
};
|
||||
let clamped = over.clamped();
|
||||
assert_eq!(clamped.max_time_secs, ResourceBudget::MAX.max_time_secs);
|
||||
assert_eq!(clamped.max_tokens, ResourceBudget::MAX.max_tokens);
|
||||
assert_eq!(
|
||||
clamped.max_cost_microdollars,
|
||||
ResourceBudget::MAX.max_cost_microdollars
|
||||
);
|
||||
assert_eq!(clamped.max_tool_calls, ResourceBudget::MAX.max_tool_calls);
|
||||
assert_eq!(
|
||||
clamped.max_external_writes,
|
||||
ResourceBudget::MAX.max_external_writes
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_budget_within_max_unchanged() {
|
||||
let within = ResourceBudget::DEFAULT;
|
||||
let clamped = within.clamped();
|
||||
assert_eq!(clamped, within);
|
||||
}
|
||||
|
||||
// --- Coherence threshold tests ---
|
||||
|
||||
#[test]
|
||||
fn coherence_thresholds_default() {
|
||||
let ct = CoherenceThresholds::default();
|
||||
assert!((ct.min_coherence_score - 0.70).abs() < f32::EPSILON);
|
||||
assert!((ct.max_contradiction_rate - 5.0).abs() < f32::EPSILON);
|
||||
assert!((ct.max_rollback_ratio - 0.20).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coherence_thresholds_strict() {
|
||||
let ct = CoherenceThresholds::STRICT;
|
||||
assert!((ct.min_coherence_score - 0.85).abs() < f32::EPSILON);
|
||||
assert!((ct.max_contradiction_rate - 2.0).abs() < f32::EPSILON);
|
||||
assert!((ct.max_rollback_ratio - 0.10).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coherence_thresholds_validate_valid() {
|
||||
assert!(CoherenceThresholds::DEFAULT.validate().is_ok());
|
||||
assert!(CoherenceThresholds::STRICT.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coherence_thresholds_validate_bad_score() {
|
||||
let ct = CoherenceThresholds {
|
||||
min_coherence_score: 1.5,
|
||||
..CoherenceThresholds::DEFAULT
|
||||
};
|
||||
assert_eq!(
|
||||
ct.validate(),
|
||||
Err(ContainerError::InvalidConfig(
|
||||
"min_coherence_score must be in [0.0, 1.0]"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coherence_thresholds_validate_negative_rate() {
|
||||
let ct = CoherenceThresholds {
|
||||
max_contradiction_rate: -1.0,
|
||||
..CoherenceThresholds::DEFAULT
|
||||
};
|
||||
assert_eq!(
|
||||
ct.validate(),
|
||||
Err(ContainerError::InvalidConfig(
|
||||
"max_contradiction_rate must be >= 0.0"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coherence_thresholds_validate_bad_ratio() {
|
||||
let ct = CoherenceThresholds {
|
||||
max_rollback_ratio: 2.0,
|
||||
..CoherenceThresholds::DEFAULT
|
||||
};
|
||||
assert_eq!(
|
||||
ct.validate(),
|
||||
Err(ContainerError::InvalidConfig(
|
||||
"max_rollback_ratio must be in [0.0, 1.0]"
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
284
crates/rvf/rvf-types/src/attestation.rs
Normal file
284
crates/rvf/rvf-types/src/attestation.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
//! Attestation types for Confidential Computing integration.
|
||||
//!
|
||||
//! These types describe hardware TEE platforms, attestation metadata,
|
||||
//! and the wire format for attestation records stored in WITNESS_SEG
|
||||
//! and CRYPTO_SEG payloads.
|
||||
|
||||
/// Hardware TEE platform identifier.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum TeePlatform {
|
||||
/// Intel SGX.
|
||||
Sgx = 0,
|
||||
/// AMD SEV-SNP.
|
||||
SevSnp = 1,
|
||||
/// Intel TDX.
|
||||
Tdx = 2,
|
||||
/// ARM CCA.
|
||||
ArmCca = 3,
|
||||
/// Software-emulated (testing only).
|
||||
SoftwareTee = 0xFE,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for TeePlatform {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Sgx),
|
||||
1 => Ok(Self::SevSnp),
|
||||
2 => Ok(Self::Tdx),
|
||||
3 => Ok(Self::ArmCca),
|
||||
0xFE => Ok(Self::SoftwareTee),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attestation witness type discriminant.
|
||||
///
|
||||
/// These extend the existing witness_type values (0x01=PROVENANCE,
|
||||
/// 0x02=COMPUTATION used by claude-flow adapter).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum AttestationWitnessType {
|
||||
/// TEE identity and measurement.
|
||||
PlatformAttestation = 0x05,
|
||||
/// Encryption key bound to TEE measurement.
|
||||
KeyBinding = 0x06,
|
||||
/// Operations performed inside the TEE.
|
||||
ComputationProof = 0x07,
|
||||
/// Chain of custody from model to TEE to RVF.
|
||||
DataProvenance = 0x08,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for AttestationWitnessType {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x05 => Ok(Self::PlatformAttestation),
|
||||
0x06 => Ok(Self::KeyBinding),
|
||||
0x07 => Ok(Self::ComputationProof),
|
||||
0x08 => Ok(Self::DataProvenance),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Key type for keys bound to a TEE measurement.
|
||||
pub const KEY_TYPE_TEE_BOUND: u8 = 4;
|
||||
|
||||
/// Wire-format attestation header (exactly 112 bytes, `repr(C)`).
|
||||
///
|
||||
/// ```text
|
||||
/// Offset Type Field
|
||||
/// 0x00 u8 platform (TeePlatform discriminant)
|
||||
/// 0x01 u8 attestation_type (AttestationWitnessType discriminant)
|
||||
/// 0x02 u16 quote_length (LE, length of opaque quote blob)
|
||||
/// 0x04 u32 reserved_0 (must be zero)
|
||||
/// 0x08 [u8; 32] measurement (MRENCLAVE / launch digest)
|
||||
/// 0x28 [u8; 32] signer_id (MRSIGNER / author key hash)
|
||||
/// 0x48 u64 timestamp_ns (LE, when attestation was captured)
|
||||
/// 0x50 [u8; 16] nonce (anti-replay nonce)
|
||||
/// 0x60 u16 svn (LE, security version number)
|
||||
/// 0x62 u16 sig_algo (LE, SignatureAlgo of the quote)
|
||||
/// 0x64 u8 flags (attestation flags)
|
||||
/// 0x65 [u8; 3] reserved_1 (must be zero)
|
||||
/// 0x68 u64 report_data_len (LE, length of custom report data)
|
||||
/// ```
|
||||
///
|
||||
/// Total: 112 bytes (0x70).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct AttestationHeader {
|
||||
/// TEE platform discriminant (see [`TeePlatform`]).
|
||||
pub platform: u8,
|
||||
/// Attestation witness type discriminant (see [`AttestationWitnessType`]).
|
||||
pub attestation_type: u8,
|
||||
/// Length of the opaque quote blob (little-endian).
|
||||
pub quote_length: u16,
|
||||
/// Reserved, must be zero.
|
||||
pub reserved_0: u32,
|
||||
/// MRENCLAVE / launch digest.
|
||||
pub measurement: [u8; 32],
|
||||
/// MRSIGNER / author key hash.
|
||||
pub signer_id: [u8; 32],
|
||||
/// Timestamp in nanoseconds when attestation was captured (little-endian).
|
||||
pub timestamp_ns: u64,
|
||||
/// Anti-replay nonce.
|
||||
pub nonce: [u8; 16],
|
||||
/// Security version number (little-endian).
|
||||
pub svn: u16,
|
||||
/// Signature algorithm of the quote (little-endian).
|
||||
pub sig_algo: u16,
|
||||
/// Attestation flags.
|
||||
pub flags: u8,
|
||||
/// Reserved, must be zero.
|
||||
pub reserved_1: [u8; 3],
|
||||
/// Length of custom report data (little-endian).
|
||||
pub report_data_len: u64,
|
||||
}
|
||||
|
||||
// Compile-time size assertion.
|
||||
const _: () = assert!(core::mem::size_of::<AttestationHeader>() == 112);
|
||||
|
||||
impl AttestationHeader {
|
||||
/// TEE is in debug mode.
|
||||
pub const FLAG_DEBUGGABLE: u8 = 0x01;
|
||||
/// Custom report data present.
|
||||
pub const FLAG_HAS_REPORT_DATA: u8 = 0x02;
|
||||
/// Combined from multiple TEEs.
|
||||
pub const FLAG_MULTI_PLATFORM: u8 = 0x04;
|
||||
|
||||
/// Create a new attestation header with all fields zeroed except
|
||||
/// `platform` and `attestation_type`.
|
||||
pub const fn new(platform: u8, attestation_type: u8) -> Self {
|
||||
Self {
|
||||
platform,
|
||||
attestation_type,
|
||||
quote_length: 0,
|
||||
reserved_0: 0,
|
||||
measurement: [0u8; 32],
|
||||
signer_id: [0u8; 32],
|
||||
timestamp_ns: 0,
|
||||
nonce: [0u8; 16],
|
||||
svn: 0,
|
||||
sig_algo: 0,
|
||||
flags: 0,
|
||||
reserved_1: [0u8; 3],
|
||||
report_data_len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the TEE is in debug mode.
|
||||
pub const fn is_debuggable(&self) -> bool {
|
||||
self.flags & Self::FLAG_DEBUGGABLE != 0
|
||||
}
|
||||
|
||||
/// Returns `true` if custom report data is present.
|
||||
pub const fn has_report_data(&self) -> bool {
|
||||
self.flags & Self::FLAG_HAS_REPORT_DATA != 0
|
||||
}
|
||||
|
||||
/// Returns the total record length:
|
||||
/// 112 (header) + report_data_len + quote_length.
|
||||
pub const fn total_record_length(&self) -> u64 {
|
||||
112 + self.report_data_len + self.quote_length as u64
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tee_platform_round_trip() {
|
||||
let variants: &[(u8, TeePlatform)] = &[
|
||||
(0, TeePlatform::Sgx),
|
||||
(1, TeePlatform::SevSnp),
|
||||
(2, TeePlatform::Tdx),
|
||||
(3, TeePlatform::ArmCca),
|
||||
(0xFE, TeePlatform::SoftwareTee),
|
||||
];
|
||||
for &(raw, expected) in variants {
|
||||
let parsed = TeePlatform::try_from(raw).unwrap();
|
||||
assert_eq!(parsed, expected);
|
||||
assert_eq!(parsed as u8, raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tee_platform_invalid() {
|
||||
assert_eq!(TeePlatform::try_from(4), Err(4));
|
||||
assert_eq!(TeePlatform::try_from(0xFF), Err(0xFF));
|
||||
assert_eq!(TeePlatform::try_from(0x80), Err(0x80));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attestation_witness_type_round_trip() {
|
||||
let variants: &[(u8, AttestationWitnessType)] = &[
|
||||
(0x05, AttestationWitnessType::PlatformAttestation),
|
||||
(0x06, AttestationWitnessType::KeyBinding),
|
||||
(0x07, AttestationWitnessType::ComputationProof),
|
||||
(0x08, AttestationWitnessType::DataProvenance),
|
||||
];
|
||||
for &(raw, expected) in variants {
|
||||
let parsed = AttestationWitnessType::try_from(raw).unwrap();
|
||||
assert_eq!(parsed, expected);
|
||||
assert_eq!(parsed as u8, raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attestation_witness_type_invalid() {
|
||||
assert_eq!(AttestationWitnessType::try_from(0x00), Err(0x00));
|
||||
assert_eq!(AttestationWitnessType::try_from(0x04), Err(0x04));
|
||||
assert_eq!(AttestationWitnessType::try_from(0x09), Err(0x09));
|
||||
assert_eq!(AttestationWitnessType::try_from(0xFF), Err(0xFF));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attestation_header_size() {
|
||||
assert_eq!(core::mem::size_of::<AttestationHeader>(), 112);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attestation_header_new() {
|
||||
let hdr = AttestationHeader::new(
|
||||
TeePlatform::SevSnp as u8,
|
||||
AttestationWitnessType::PlatformAttestation as u8,
|
||||
);
|
||||
assert_eq!(hdr.platform, 1);
|
||||
assert_eq!(hdr.attestation_type, 0x05);
|
||||
assert_eq!(hdr.quote_length, 0);
|
||||
assert_eq!(hdr.reserved_0, 0);
|
||||
assert_eq!(hdr.measurement, [0u8; 32]);
|
||||
assert_eq!(hdr.signer_id, [0u8; 32]);
|
||||
assert_eq!(hdr.timestamp_ns, 0);
|
||||
assert_eq!(hdr.nonce, [0u8; 16]);
|
||||
assert_eq!(hdr.svn, 0);
|
||||
assert_eq!(hdr.sig_algo, 0);
|
||||
assert_eq!(hdr.flags, 0);
|
||||
assert_eq!(hdr.reserved_1, [0u8; 3]);
|
||||
assert_eq!(hdr.report_data_len, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_is_debuggable() {
|
||||
let mut hdr = AttestationHeader::new(0, 0);
|
||||
assert!(!hdr.is_debuggable());
|
||||
hdr.flags = AttestationHeader::FLAG_DEBUGGABLE;
|
||||
assert!(hdr.is_debuggable());
|
||||
// combined flags
|
||||
hdr.flags = AttestationHeader::FLAG_DEBUGGABLE | AttestationHeader::FLAG_HAS_REPORT_DATA;
|
||||
assert!(hdr.is_debuggable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_has_report_data() {
|
||||
let mut hdr = AttestationHeader::new(0, 0);
|
||||
assert!(!hdr.has_report_data());
|
||||
hdr.flags = AttestationHeader::FLAG_HAS_REPORT_DATA;
|
||||
assert!(hdr.has_report_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_record_length() {
|
||||
let mut hdr = AttestationHeader::new(0, 0);
|
||||
assert_eq!(hdr.total_record_length(), 112);
|
||||
|
||||
hdr.quote_length = 256;
|
||||
assert_eq!(hdr.total_record_length(), 112 + 256);
|
||||
|
||||
hdr.report_data_len = 64;
|
||||
assert_eq!(hdr.total_record_length(), 112 + 64 + 256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_type_tee_bound_value() {
|
||||
assert_eq!(KEY_TYPE_TEE_BOUND, 4);
|
||||
}
|
||||
}
|
||||
44
crates/rvf/rvf-types/src/checksum.rs
Normal file
44
crates/rvf/rvf-types/src/checksum.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! Checksum / hash algorithm identifiers.
|
||||
|
||||
/// Identifies the hash algorithm used for segment content verification.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum ChecksumAlgo {
|
||||
/// CRC32C (SSE4.2 hardware-accelerated). Output: 4 bytes, zero-padded to 16.
|
||||
Crc32c = 0,
|
||||
/// XXH3-128. Output: 16 bytes. Fast, good distribution.
|
||||
Xxh3_128 = 1,
|
||||
/// SHAKE-256 (first 128 bits). Post-quantum safe, cryptographic.
|
||||
Shake256 = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ChecksumAlgo {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Crc32c),
|
||||
1 => Ok(Self::Xxh3_128),
|
||||
2 => Ok(Self::Shake256),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
assert_eq!(ChecksumAlgo::try_from(0), Ok(ChecksumAlgo::Crc32c));
|
||||
assert_eq!(ChecksumAlgo::try_from(1), Ok(ChecksumAlgo::Xxh3_128));
|
||||
assert_eq!(ChecksumAlgo::try_from(2), Ok(ChecksumAlgo::Shake256));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_value() {
|
||||
assert_eq!(ChecksumAlgo::try_from(3), Err(3));
|
||||
}
|
||||
}
|
||||
48
crates/rvf/rvf-types/src/compression.rs
Normal file
48
crates/rvf/rvf-types/src/compression.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Compression algorithm identifiers.
|
||||
|
||||
/// Identifies the compression algorithm applied to a segment payload.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum CompressionAlgo {
|
||||
/// No compression.
|
||||
None = 0,
|
||||
/// LZ4 block compression (~4 GB/s decompress).
|
||||
Lz4 = 1,
|
||||
/// Zstandard compression (~1.5 GB/s decompress, higher ratio).
|
||||
Zstd = 2,
|
||||
/// Domain-specific custom compression.
|
||||
Custom = 3,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for CompressionAlgo {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::None),
|
||||
1 => Ok(Self::Lz4),
|
||||
2 => Ok(Self::Zstd),
|
||||
3 => Ok(Self::Custom),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for raw in 0..=3u8 {
|
||||
let algo = CompressionAlgo::try_from(raw).unwrap();
|
||||
assert_eq!(algo as u8, raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_value() {
|
||||
assert_eq!(CompressionAlgo::try_from(4), Err(4));
|
||||
}
|
||||
}
|
||||
52
crates/rvf/rvf-types/src/constants.rs
Normal file
52
crates/rvf/rvf-types/src/constants.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Magic numbers, alignment requirements, and size limits for the RVF format.
|
||||
|
||||
/// Segment header magic: "RVFS" in ASCII (little-endian u32).
|
||||
pub const SEGMENT_MAGIC: u32 = 0x5256_4653;
|
||||
|
||||
/// Root manifest magic: "RVM0" in ASCII (little-endian u32).
|
||||
pub const ROOT_MANIFEST_MAGIC: u32 = 0x5256_4D30;
|
||||
|
||||
/// All segments must start at a 64-byte aligned boundary (AVX-512 / cache-line width).
|
||||
pub const SEGMENT_ALIGNMENT: usize = 64;
|
||||
|
||||
/// The Level 0 root manifest is always exactly 4096 bytes (one OS page / disk sector).
|
||||
pub const ROOT_MANIFEST_SIZE: usize = 4096;
|
||||
|
||||
/// Maximum payload size for a single segment (4 GiB).
|
||||
pub const MAX_SEGMENT_PAYLOAD: u64 = 4 * 1024 * 1024 * 1024;
|
||||
|
||||
/// Size of the segment header in bytes.
|
||||
pub const SEGMENT_HEADER_SIZE: usize = 64;
|
||||
|
||||
/// Current segment format version.
|
||||
pub const SEGMENT_VERSION: u8 = 1;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
// "RVFS" => 0x52 0x56 0x46 0x53
|
||||
let bytes = SEGMENT_MAGIC.to_le_bytes();
|
||||
assert_eq!(&bytes, b"SFVR"); // LE representation: 0x53, 0x46, 0x56, 0x52
|
||||
let bytes_be = SEGMENT_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVFS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_manifest_magic_bytes() {
|
||||
let bytes_be = ROOT_MANIFEST_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVM0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment_is_power_of_two() {
|
||||
assert!(SEGMENT_ALIGNMENT.is_power_of_two());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_payload_is_4gb() {
|
||||
assert_eq!(MAX_SEGMENT_PAYLOAD, 0x1_0000_0000);
|
||||
}
|
||||
}
|
||||
243
crates/rvf/rvf-types/src/cow_map.rs
Normal file
243
crates/rvf/rvf-types/src/cow_map.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
//! COW_MAP_SEG (0x20) types for the RVF computational container.
|
||||
//!
|
||||
//! Defines the 64-byte `CowMapHeader` and associated enums per ADR-031.
|
||||
//! The COW_MAP_SEG tracks copy-on-write cluster mappings, enabling
|
||||
//! branching and snapshotting of vector data without full duplication.
|
||||
|
||||
use crate::error::RvfError;
|
||||
|
||||
/// Magic number for `CowMapHeader`: "RVCM" in big-endian.
|
||||
pub const COWMAP_MAGIC: u32 = 0x5256_434D;
|
||||
|
||||
/// Cluster map storage format.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum MapFormat {
|
||||
/// Simple flat array of cluster entries.
|
||||
FlatArray = 0,
|
||||
/// Adaptive Radix Tree for sparse mappings.
|
||||
ArtTree = 1,
|
||||
/// Extent list for contiguous ranges.
|
||||
ExtentList = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for MapFormat {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::FlatArray),
|
||||
1 => Ok(Self::ArtTree),
|
||||
2 => Ok(Self::ExtentList),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "MapFormat",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry in the COW cluster map.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum CowMapEntry {
|
||||
/// Cluster has been written locally at the given offset.
|
||||
LocalOffset(u64),
|
||||
/// Cluster data lives in the parent file.
|
||||
ParentRef,
|
||||
/// Cluster has not been allocated.
|
||||
Unallocated,
|
||||
}
|
||||
|
||||
/// 64-byte header for COW_MAP_SEG payloads.
|
||||
///
|
||||
/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
|
||||
/// little-endian on the wire.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct CowMapHeader {
|
||||
/// Magic: `COWMAP_MAGIC` (0x5256434D, "RVCM").
|
||||
pub magic: u32,
|
||||
/// CowMapHeader format version (currently 1).
|
||||
pub version: u16,
|
||||
/// Map storage format (see `MapFormat`).
|
||||
pub map_format: u8,
|
||||
/// Compression policy for COW clusters.
|
||||
pub compression_policy: u8,
|
||||
/// Cluster size in bytes (must be power of 2, SIMD aligned).
|
||||
pub cluster_size_bytes: u32,
|
||||
/// Number of vectors per cluster.
|
||||
pub vectors_per_cluster: u32,
|
||||
/// UUID of the base (parent) file.
|
||||
pub base_file_id: [u8; 16],
|
||||
/// SHAKE-256-256 hash of the base file.
|
||||
pub base_file_hash: [u8; 32],
|
||||
}
|
||||
|
||||
// Compile-time assertion: CowMapHeader must be exactly 64 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<CowMapHeader>() == 64);
|
||||
|
||||
impl CowMapHeader {
|
||||
/// Serialize the header to a 64-byte little-endian array.
|
||||
pub fn to_bytes(&self) -> [u8; 64] {
|
||||
let mut buf = [0u8; 64];
|
||||
buf[0x00..0x04].copy_from_slice(&self.magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.version.to_le_bytes());
|
||||
buf[0x06] = self.map_format;
|
||||
buf[0x07] = self.compression_policy;
|
||||
buf[0x08..0x0C].copy_from_slice(&self.cluster_size_bytes.to_le_bytes());
|
||||
buf[0x0C..0x10].copy_from_slice(&self.vectors_per_cluster.to_le_bytes());
|
||||
buf[0x10..0x20].copy_from_slice(&self.base_file_id);
|
||||
buf[0x20..0x40].copy_from_slice(&self.base_file_hash);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize a `CowMapHeader` from a 64-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 64]) -> Result<Self, RvfError> {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != COWMAP_MAGIC {
|
||||
return Err(RvfError::BadMagic {
|
||||
expected: COWMAP_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
|
||||
let version = u16::from_le_bytes([data[0x04], data[0x05]]);
|
||||
let map_format = data[0x06];
|
||||
let cluster_size_bytes =
|
||||
u32::from_le_bytes([data[0x08], data[0x09], data[0x0A], data[0x0B]]);
|
||||
let vectors_per_cluster =
|
||||
u32::from_le_bytes([data[0x0C], data[0x0D], data[0x0E], data[0x0F]]);
|
||||
|
||||
// Validate map_format is a known enum value
|
||||
let _ = MapFormat::try_from(map_format)?;
|
||||
|
||||
// Validate cluster_size_bytes is a power of 2 and non-zero
|
||||
if cluster_size_bytes == 0 || !cluster_size_bytes.is_power_of_two() {
|
||||
return Err(RvfError::InvalidEnumValue {
|
||||
type_name: "CowMapHeader::cluster_size_bytes",
|
||||
value: cluster_size_bytes as u64,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate vectors_per_cluster is non-zero (prevents division by zero)
|
||||
if vectors_per_cluster == 0 {
|
||||
return Err(RvfError::InvalidEnumValue {
|
||||
type_name: "CowMapHeader::vectors_per_cluster",
|
||||
value: 0,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version,
|
||||
map_format,
|
||||
compression_policy: data[0x07],
|
||||
cluster_size_bytes,
|
||||
vectors_per_cluster,
|
||||
base_file_id: {
|
||||
let mut id = [0u8; 16];
|
||||
id.copy_from_slice(&data[0x10..0x20]);
|
||||
id
|
||||
},
|
||||
base_file_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x20..0x40]);
|
||||
h
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_header() -> CowMapHeader {
|
||||
CowMapHeader {
|
||||
magic: COWMAP_MAGIC,
|
||||
version: 1,
|
||||
map_format: MapFormat::FlatArray as u8,
|
||||
compression_policy: 0,
|
||||
cluster_size_bytes: 4096,
|
||||
vectors_per_cluster: 64,
|
||||
base_file_id: [0xAA; 16],
|
||||
base_file_hash: [0xBB; 32],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_64() {
|
||||
assert_eq!(core::mem::size_of::<CowMapHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
let bytes_be = COWMAP_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVCM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_header();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = CowMapHeader::from_bytes(&bytes).expect("from_bytes should succeed");
|
||||
|
||||
assert_eq!(decoded.magic, COWMAP_MAGIC);
|
||||
assert_eq!(decoded.version, 1);
|
||||
assert_eq!(decoded.map_format, MapFormat::FlatArray as u8);
|
||||
assert_eq!(decoded.compression_policy, 0);
|
||||
assert_eq!(decoded.cluster_size_bytes, 4096);
|
||||
assert_eq!(decoded.vectors_per_cluster, 64);
|
||||
assert_eq!(decoded.base_file_id, [0xAA; 16]);
|
||||
assert_eq!(decoded.base_file_hash, [0xBB; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_magic_returns_error() {
|
||||
let mut bytes = sample_header().to_bytes();
|
||||
bytes[0] = 0x00; // corrupt magic
|
||||
let err = CowMapHeader::from_bytes(&bytes).unwrap_err();
|
||||
match err {
|
||||
RvfError::BadMagic { expected, .. } => assert_eq!(expected, COWMAP_MAGIC),
|
||||
other => panic!("expected BadMagic, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let h = sample_header();
|
||||
let base = &h as *const _ as usize;
|
||||
|
||||
assert_eq!(&h.magic as *const _ as usize - base, 0x00);
|
||||
assert_eq!(&h.version as *const _ as usize - base, 0x04);
|
||||
assert_eq!(&h.map_format as *const _ as usize - base, 0x06);
|
||||
assert_eq!(&h.compression_policy as *const _ as usize - base, 0x07);
|
||||
assert_eq!(&h.cluster_size_bytes as *const _ as usize - base, 0x08);
|
||||
assert_eq!(&h.vectors_per_cluster as *const _ as usize - base, 0x0C);
|
||||
assert_eq!(&h.base_file_id as *const _ as usize - base, 0x10);
|
||||
assert_eq!(&h.base_file_hash as *const _ as usize - base, 0x20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_format_try_from() {
|
||||
assert_eq!(MapFormat::try_from(0), Ok(MapFormat::FlatArray));
|
||||
assert_eq!(MapFormat::try_from(1), Ok(MapFormat::ArtTree));
|
||||
assert_eq!(MapFormat::try_from(2), Ok(MapFormat::ExtentList));
|
||||
assert!(MapFormat::try_from(3).is_err());
|
||||
assert!(MapFormat::try_from(0xFF).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cow_map_entry_variants() {
|
||||
let local = CowMapEntry::LocalOffset(0x1000);
|
||||
let parent = CowMapEntry::ParentRef;
|
||||
let unalloc = CowMapEntry::Unallocated;
|
||||
|
||||
assert_eq!(local, CowMapEntry::LocalOffset(0x1000));
|
||||
assert_eq!(parent, CowMapEntry::ParentRef);
|
||||
assert_eq!(unalloc, CowMapEntry::Unallocated);
|
||||
assert_ne!(local, parent);
|
||||
}
|
||||
}
|
||||
191
crates/rvf/rvf-types/src/dashboard.rs
Normal file
191
crates/rvf/rvf-types/src/dashboard.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
//! DASHBOARD_SEG (0x11) types for the RVF computational container.
|
||||
//!
|
||||
//! Defines the 64-byte `DashboardHeader` and associated constants per ADR-040.
|
||||
//! The DASHBOARD_SEG embeds a pre-built web dashboard (e.g. Vite + Three.js)
|
||||
//! that the RVF HTTP server can serve at `/`.
|
||||
|
||||
use crate::error::RvfError;
|
||||
|
||||
/// Magic number for `DashboardHeader`: "RVDB" in big-endian.
|
||||
pub const DASHBOARD_MAGIC: u32 = 0x5256_4442;
|
||||
|
||||
/// Maximum dashboard bundle size (64 MiB).
|
||||
pub const DASHBOARD_MAX_SIZE: u64 = 64 * 1024 * 1024;
|
||||
|
||||
/// 64-byte header for DASHBOARD_SEG payloads.
|
||||
///
|
||||
/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
|
||||
/// little-endian on the wire.
|
||||
///
|
||||
/// Payload layout after header:
|
||||
/// `[entry_path_bytes | file_table | file_data...]`
|
||||
///
|
||||
/// File table: array of `(path_len: u16, data_offset: u64, data_size: u64, path_bytes: [u8])`
|
||||
/// File data: concatenated raw file contents.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct DashboardHeader {
|
||||
/// Magic: `DASHBOARD_MAGIC` (0x52564442, "RVDB").
|
||||
pub dashboard_magic: u32,
|
||||
/// DashboardHeader format version (currently 1).
|
||||
pub header_version: u16,
|
||||
/// UI framework: 0=threejs, 1=react, 2=custom.
|
||||
pub ui_framework: u8,
|
||||
/// Compression: 0=none, 1=gzip, 2=brotli.
|
||||
pub compression: u8,
|
||||
/// Total uncompressed bundle size in bytes.
|
||||
pub bundle_size: u64,
|
||||
/// Number of files in the bundle.
|
||||
pub file_count: u32,
|
||||
/// Length of the entry point path string.
|
||||
pub entry_path_len: u16,
|
||||
/// Reserved padding.
|
||||
pub reserved: u16,
|
||||
/// Build timestamp (unix epoch seconds).
|
||||
pub build_timestamp: u64,
|
||||
/// SHAKE-256-256 of the entire bundle payload.
|
||||
pub content_hash: [u8; 32],
|
||||
}
|
||||
|
||||
// Compile-time assertion: DashboardHeader must be exactly 64 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<DashboardHeader>() == 64);
|
||||
|
||||
impl DashboardHeader {
|
||||
/// Serialize the header to a 64-byte little-endian array.
|
||||
pub fn to_bytes(&self) -> [u8; 64] {
|
||||
let mut buf = [0u8; 64];
|
||||
buf[0x00..0x04].copy_from_slice(&self.dashboard_magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.header_version.to_le_bytes());
|
||||
buf[0x06] = self.ui_framework;
|
||||
buf[0x07] = self.compression;
|
||||
buf[0x08..0x10].copy_from_slice(&self.bundle_size.to_le_bytes());
|
||||
buf[0x10..0x14].copy_from_slice(&self.file_count.to_le_bytes());
|
||||
buf[0x14..0x16].copy_from_slice(&self.entry_path_len.to_le_bytes());
|
||||
buf[0x16..0x18].copy_from_slice(&self.reserved.to_le_bytes());
|
||||
buf[0x18..0x20].copy_from_slice(&self.build_timestamp.to_le_bytes());
|
||||
buf[0x20..0x40].copy_from_slice(&self.content_hash);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize a `DashboardHeader` from a 64-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 64]) -> Result<Self, RvfError> {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != DASHBOARD_MAGIC {
|
||||
return Err(RvfError::BadMagic {
|
||||
expected: DASHBOARD_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
dashboard_magic: magic,
|
||||
header_version: u16::from_le_bytes([data[0x04], data[0x05]]),
|
||||
ui_framework: data[0x06],
|
||||
compression: data[0x07],
|
||||
bundle_size: u64::from_le_bytes([
|
||||
data[0x08], data[0x09], data[0x0A], data[0x0B], data[0x0C], data[0x0D], data[0x0E],
|
||||
data[0x0F],
|
||||
]),
|
||||
file_count: u32::from_le_bytes([data[0x10], data[0x11], data[0x12], data[0x13]]),
|
||||
entry_path_len: u16::from_le_bytes([data[0x14], data[0x15]]),
|
||||
reserved: u16::from_le_bytes([data[0x16], data[0x17]]),
|
||||
build_timestamp: u64::from_le_bytes([
|
||||
data[0x18], data[0x19], data[0x1A], data[0x1B], data[0x1C], data[0x1D], data[0x1E],
|
||||
data[0x1F],
|
||||
]),
|
||||
content_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x20..0x40]);
|
||||
h
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_header() -> DashboardHeader {
|
||||
DashboardHeader {
|
||||
dashboard_magic: DASHBOARD_MAGIC,
|
||||
header_version: 1,
|
||||
ui_framework: 0, // threejs
|
||||
compression: 0, // none
|
||||
bundle_size: 524288,
|
||||
file_count: 12,
|
||||
entry_path_len: 10,
|
||||
reserved: 0,
|
||||
build_timestamp: 1_700_000_000,
|
||||
content_hash: [0xAB; 32],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_64() {
|
||||
assert_eq!(core::mem::size_of::<DashboardHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
let bytes_be = DASHBOARD_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVDB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_header();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = DashboardHeader::from_bytes(&bytes).expect("from_bytes should succeed");
|
||||
|
||||
assert_eq!(decoded.dashboard_magic, DASHBOARD_MAGIC);
|
||||
assert_eq!(decoded.header_version, 1);
|
||||
assert_eq!(decoded.ui_framework, 0);
|
||||
assert_eq!(decoded.compression, 0);
|
||||
assert_eq!(decoded.bundle_size, 524288);
|
||||
assert_eq!(decoded.file_count, 12);
|
||||
assert_eq!(decoded.entry_path_len, 10);
|
||||
assert_eq!(decoded.reserved, 0);
|
||||
assert_eq!(decoded.build_timestamp, 1_700_000_000);
|
||||
assert_eq!(decoded.content_hash, [0xAB; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_magic_returns_error() {
|
||||
let mut bytes = sample_header().to_bytes();
|
||||
bytes[0] = 0x00; // corrupt magic
|
||||
let err = DashboardHeader::from_bytes(&bytes).unwrap_err();
|
||||
match err {
|
||||
RvfError::BadMagic { expected, .. } => assert_eq!(expected, DASHBOARD_MAGIC),
|
||||
other => panic!("expected BadMagic, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let h = sample_header();
|
||||
let base = &h as *const _ as usize;
|
||||
|
||||
assert_eq!(&h.dashboard_magic as *const _ as usize - base, 0x00);
|
||||
assert_eq!(&h.header_version as *const _ as usize - base, 0x04);
|
||||
assert_eq!(&h.ui_framework as *const _ as usize - base, 0x06);
|
||||
assert_eq!(&h.compression as *const _ as usize - base, 0x07);
|
||||
assert_eq!(&h.bundle_size as *const _ as usize - base, 0x08);
|
||||
assert_eq!(&h.file_count as *const _ as usize - base, 0x10);
|
||||
assert_eq!(&h.entry_path_len as *const _ as usize - base, 0x14);
|
||||
assert_eq!(&h.reserved as *const _ as usize - base, 0x16);
|
||||
assert_eq!(&h.build_timestamp as *const _ as usize - base, 0x18);
|
||||
assert_eq!(&h.content_hash as *const _ as usize - base, 0x20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_bundle_size_round_trip() {
|
||||
let mut h = sample_header();
|
||||
h.bundle_size = DASHBOARD_MAX_SIZE;
|
||||
h.file_count = 500;
|
||||
let bytes = h.to_bytes();
|
||||
let decoded = DashboardHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.bundle_size, DASHBOARD_MAX_SIZE);
|
||||
assert_eq!(decoded.file_count, 500);
|
||||
}
|
||||
}
|
||||
89
crates/rvf/rvf-types/src/data_type.rs
Normal file
89
crates/rvf/rvf-types/src/data_type.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
//! Vector data type discriminator.
|
||||
|
||||
/// Identifies the numeric encoding of vector elements.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum DataType {
|
||||
/// 32-bit IEEE 754 float.
|
||||
F32 = 0,
|
||||
/// 16-bit IEEE 754 half-precision float.
|
||||
F16 = 1,
|
||||
/// Brain floating point (bfloat16).
|
||||
BF16 = 2,
|
||||
/// Signed 8-bit integer (scalar quantized).
|
||||
I8 = 3,
|
||||
/// Unsigned 8-bit integer.
|
||||
U8 = 4,
|
||||
/// 4-bit integer (packed, 2 per byte).
|
||||
I4 = 5,
|
||||
/// 1-bit binary (packed, 8 per byte).
|
||||
Binary = 6,
|
||||
/// Product-quantized codes.
|
||||
PQ = 7,
|
||||
/// Custom encoding (see QUANT_SEG for details).
|
||||
Custom = 8,
|
||||
}
|
||||
|
||||
impl DataType {
|
||||
/// Returns the number of bits per element, or `None` for variable-width types.
|
||||
pub const fn bits_per_element(self) -> Option<u32> {
|
||||
match self {
|
||||
Self::F32 => Some(32),
|
||||
Self::F16 => Some(16),
|
||||
Self::BF16 => Some(16),
|
||||
Self::I8 => Some(8),
|
||||
Self::U8 => Some(8),
|
||||
Self::I4 => Some(4),
|
||||
Self::Binary => Some(1),
|
||||
Self::PQ | Self::Custom => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for DataType {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::F32),
|
||||
1 => Ok(Self::F16),
|
||||
2 => Ok(Self::BF16),
|
||||
3 => Ok(Self::I8),
|
||||
4 => Ok(Self::U8),
|
||||
5 => Ok(Self::I4),
|
||||
6 => Ok(Self::Binary),
|
||||
7 => Ok(Self::PQ),
|
||||
8 => Ok(Self::Custom),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for raw in 0..=8u8 {
|
||||
let dt = DataType::try_from(raw).unwrap();
|
||||
assert_eq!(dt as u8, raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_value() {
|
||||
assert_eq!(DataType::try_from(9), Err(9));
|
||||
assert_eq!(DataType::try_from(255), Err(255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bits_per_element() {
|
||||
assert_eq!(DataType::F32.bits_per_element(), Some(32));
|
||||
assert_eq!(DataType::F16.bits_per_element(), Some(16));
|
||||
assert_eq!(DataType::I4.bits_per_element(), Some(4));
|
||||
assert_eq!(DataType::Binary.bits_per_element(), Some(1));
|
||||
assert_eq!(DataType::PQ.bits_per_element(), None);
|
||||
}
|
||||
}
|
||||
203
crates/rvf/rvf-types/src/delta.rs
Normal file
203
crates/rvf/rvf-types/src/delta.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
//! DELTA_SEG (0x23) types for the RVF computational container.
|
||||
//!
|
||||
//! Defines the 64-byte `DeltaHeader` and associated enums per ADR-031.
|
||||
//! The DELTA_SEG stores sparse delta patches between clusters,
|
||||
//! enabling efficient incremental updates without full cluster rewrites.
|
||||
|
||||
use crate::error::RvfError;
|
||||
|
||||
/// Magic number for `DeltaHeader`: "RVDL" in big-endian.
|
||||
pub const DELTA_MAGIC: u32 = 0x5256_444C;
|
||||
|
||||
/// Delta encoding strategy.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum DeltaEncoding {
|
||||
/// Sparse row patches (individual vector updates).
|
||||
SparseRows = 0,
|
||||
/// Low-rank approximation of the delta.
|
||||
LowRank = 1,
|
||||
/// Full cluster patch (complete replacement).
|
||||
FullPatch = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for DeltaEncoding {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::SparseRows),
|
||||
1 => Ok(Self::LowRank),
|
||||
2 => Ok(Self::FullPatch),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "DeltaEncoding",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 64-byte header for DELTA_SEG payloads.
|
||||
///
|
||||
/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
|
||||
/// little-endian on the wire.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct DeltaHeader {
|
||||
/// Magic: `DELTA_MAGIC` (0x5256444C, "RVDL").
|
||||
pub magic: u32,
|
||||
/// DeltaHeader format version (currently 1).
|
||||
pub version: u16,
|
||||
/// Delta encoding strategy (see `DeltaEncoding`).
|
||||
pub encoding: u8,
|
||||
/// Padding (must be zero).
|
||||
pub _pad: u8,
|
||||
/// Cluster ID that this delta applies to.
|
||||
pub base_cluster_id: u32,
|
||||
/// Number of vectors affected by this delta.
|
||||
pub affected_count: u32,
|
||||
/// Size of the delta payload in bytes.
|
||||
pub delta_size: u64,
|
||||
/// SHAKE-256-256 hash of the delta payload.
|
||||
pub delta_hash: [u8; 32],
|
||||
/// Reserved (must be zero).
|
||||
pub _reserved: [u8; 8],
|
||||
}
|
||||
|
||||
// Compile-time assertion: DeltaHeader must be exactly 64 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<DeltaHeader>() == 64);
|
||||
|
||||
impl DeltaHeader {
|
||||
/// Serialize the header to a 64-byte little-endian array.
|
||||
pub fn to_bytes(&self) -> [u8; 64] {
|
||||
let mut buf = [0u8; 64];
|
||||
buf[0x00..0x04].copy_from_slice(&self.magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.version.to_le_bytes());
|
||||
buf[0x06] = self.encoding;
|
||||
buf[0x07] = self._pad;
|
||||
buf[0x08..0x0C].copy_from_slice(&self.base_cluster_id.to_le_bytes());
|
||||
buf[0x0C..0x10].copy_from_slice(&self.affected_count.to_le_bytes());
|
||||
buf[0x10..0x18].copy_from_slice(&self.delta_size.to_le_bytes());
|
||||
buf[0x18..0x38].copy_from_slice(&self.delta_hash);
|
||||
buf[0x38..0x40].copy_from_slice(&self._reserved);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize a `DeltaHeader` from a 64-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 64]) -> Result<Self, RvfError> {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != DELTA_MAGIC {
|
||||
return Err(RvfError::BadMagic {
|
||||
expected: DELTA_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version: u16::from_le_bytes([data[0x04], data[0x05]]),
|
||||
encoding: data[0x06],
|
||||
_pad: data[0x07],
|
||||
base_cluster_id: u32::from_le_bytes([data[0x08], data[0x09], data[0x0A], data[0x0B]]),
|
||||
affected_count: u32::from_le_bytes([data[0x0C], data[0x0D], data[0x0E], data[0x0F]]),
|
||||
delta_size: u64::from_le_bytes([
|
||||
data[0x10], data[0x11], data[0x12], data[0x13], data[0x14], data[0x15], data[0x16],
|
||||
data[0x17],
|
||||
]),
|
||||
delta_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x18..0x38]);
|
||||
h
|
||||
},
|
||||
_reserved: {
|
||||
let mut r = [0u8; 8];
|
||||
r.copy_from_slice(&data[0x38..0x40]);
|
||||
r
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_header() -> DeltaHeader {
|
||||
DeltaHeader {
|
||||
magic: DELTA_MAGIC,
|
||||
version: 1,
|
||||
encoding: DeltaEncoding::SparseRows as u8,
|
||||
_pad: 0,
|
||||
base_cluster_id: 42,
|
||||
affected_count: 10,
|
||||
delta_size: 2048,
|
||||
delta_hash: [0xDD; 32],
|
||||
_reserved: [0; 8],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_64() {
|
||||
assert_eq!(core::mem::size_of::<DeltaHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
let bytes_be = DELTA_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVDL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_header();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = DeltaHeader::from_bytes(&bytes).expect("from_bytes should succeed");
|
||||
|
||||
assert_eq!(decoded.magic, DELTA_MAGIC);
|
||||
assert_eq!(decoded.version, 1);
|
||||
assert_eq!(decoded.encoding, DeltaEncoding::SparseRows as u8);
|
||||
assert_eq!(decoded._pad, 0);
|
||||
assert_eq!(decoded.base_cluster_id, 42);
|
||||
assert_eq!(decoded.affected_count, 10);
|
||||
assert_eq!(decoded.delta_size, 2048);
|
||||
assert_eq!(decoded.delta_hash, [0xDD; 32]);
|
||||
assert_eq!(decoded._reserved, [0; 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_magic_returns_error() {
|
||||
let mut bytes = sample_header().to_bytes();
|
||||
bytes[0] = 0x00; // corrupt magic
|
||||
let err = DeltaHeader::from_bytes(&bytes).unwrap_err();
|
||||
match err {
|
||||
RvfError::BadMagic { expected, .. } => assert_eq!(expected, DELTA_MAGIC),
|
||||
other => panic!("expected BadMagic, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let h = sample_header();
|
||||
let base = &h as *const _ as usize;
|
||||
|
||||
assert_eq!(&h.magic as *const _ as usize - base, 0x00);
|
||||
assert_eq!(&h.version as *const _ as usize - base, 0x04);
|
||||
assert_eq!(&h.encoding as *const _ as usize - base, 0x06);
|
||||
assert_eq!(&h._pad as *const _ as usize - base, 0x07);
|
||||
assert_eq!(&h.base_cluster_id as *const _ as usize - base, 0x08);
|
||||
assert_eq!(&h.affected_count as *const _ as usize - base, 0x0C);
|
||||
assert_eq!(&h.delta_size as *const _ as usize - base, 0x10);
|
||||
assert_eq!(&h.delta_hash as *const _ as usize - base, 0x18);
|
||||
assert_eq!(&h._reserved as *const _ as usize - base, 0x38);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_encoding_try_from() {
|
||||
assert_eq!(DeltaEncoding::try_from(0), Ok(DeltaEncoding::SparseRows));
|
||||
assert_eq!(DeltaEncoding::try_from(1), Ok(DeltaEncoding::LowRank));
|
||||
assert_eq!(DeltaEncoding::try_from(2), Ok(DeltaEncoding::FullPatch));
|
||||
assert!(DeltaEncoding::try_from(3).is_err());
|
||||
assert!(DeltaEncoding::try_from(0xFF).is_err());
|
||||
}
|
||||
}
|
||||
333
crates/rvf/rvf-types/src/ebpf.rs
Normal file
333
crates/rvf/rvf-types/src/ebpf.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
//! EBPF_SEG (0x0F) types for the RVF computational container.
|
||||
//!
|
||||
//! Defines the 64-byte `EbpfHeader` and associated enums per ADR-030.
|
||||
//! The EBPF_SEG embeds an eBPF program for kernel-level fast-path
|
||||
//! vector distance computation (L0 cache in BPF maps).
|
||||
|
||||
use crate::error::RvfError;
|
||||
|
||||
/// Magic number for `EbpfHeader`: "RVBP" in big-endian.
|
||||
pub const EBPF_MAGIC: u32 = 0x5256_4250;
|
||||
|
||||
/// eBPF program type classification.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum EbpfProgramType {
|
||||
/// XDP program for distance computation on packets.
|
||||
XdpDistance = 0x00,
|
||||
/// TC classifier for query routing.
|
||||
TcFilter = 0x01,
|
||||
/// Socket filter for query preprocessing.
|
||||
SocketFilter = 0x02,
|
||||
/// Tracepoint for performance monitoring.
|
||||
Tracepoint = 0x03,
|
||||
/// Kprobe for dynamic instrumentation.
|
||||
Kprobe = 0x04,
|
||||
/// Cgroup socket buffer filter.
|
||||
CgroupSkb = 0x05,
|
||||
/// Custom program type.
|
||||
Custom = 0xFF,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for EbpfProgramType {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::XdpDistance),
|
||||
0x01 => Ok(Self::TcFilter),
|
||||
0x02 => Ok(Self::SocketFilter),
|
||||
0x03 => Ok(Self::Tracepoint),
|
||||
0x04 => Ok(Self::Kprobe),
|
||||
0x05 => Ok(Self::CgroupSkb),
|
||||
0xFF => Ok(Self::Custom),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "EbpfProgramType",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// eBPF attach point classification.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum EbpfAttachType {
|
||||
/// XDP hook on NIC ingress.
|
||||
XdpIngress = 0x00,
|
||||
/// TC ingress qdisc.
|
||||
TcIngress = 0x01,
|
||||
/// TC egress qdisc.
|
||||
TcEgress = 0x02,
|
||||
/// Socket filter attachment.
|
||||
SocketFilter = 0x03,
|
||||
/// Cgroup ingress.
|
||||
CgroupIngress = 0x04,
|
||||
/// Cgroup egress.
|
||||
CgroupEgress = 0x05,
|
||||
/// No automatic attachment.
|
||||
None = 0xFF,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for EbpfAttachType {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::XdpIngress),
|
||||
0x01 => Ok(Self::TcIngress),
|
||||
0x02 => Ok(Self::TcEgress),
|
||||
0x03 => Ok(Self::SocketFilter),
|
||||
0x04 => Ok(Self::CgroupIngress),
|
||||
0x05 => Ok(Self::CgroupEgress),
|
||||
0xFF => Ok(Self::None),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "EbpfAttachType",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 64-byte header for EBPF_SEG payloads.
|
||||
///
|
||||
/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
|
||||
/// little-endian on the wire.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct EbpfHeader {
|
||||
/// Magic: `EBPF_MAGIC` (0x52564250, "RVBP").
|
||||
pub ebpf_magic: u32,
|
||||
/// EbpfHeader format version (currently 1).
|
||||
pub header_version: u16,
|
||||
/// eBPF program type (see `EbpfProgramType`).
|
||||
pub program_type: u8,
|
||||
/// eBPF attach point (see `EbpfAttachType`).
|
||||
pub attach_type: u8,
|
||||
/// Bitfield flags for the eBPF program.
|
||||
pub program_flags: u32,
|
||||
/// Number of BPF instructions (max 65535).
|
||||
pub insn_count: u16,
|
||||
/// Maximum vector dimension this program handles.
|
||||
pub max_dimension: u16,
|
||||
/// ELF object size (bytes).
|
||||
pub program_size: u64,
|
||||
/// Number of BPF maps defined.
|
||||
pub map_count: u32,
|
||||
/// BTF (BPF Type Format) section size.
|
||||
pub btf_size: u32,
|
||||
/// SHAKE-256-256 of the ELF object.
|
||||
pub program_hash: [u8; 32],
|
||||
}
|
||||
|
||||
// Compile-time assertion: EbpfHeader must be exactly 64 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<EbpfHeader>() == 64);
|
||||
|
||||
impl EbpfHeader {
|
||||
/// Serialize the header to a 64-byte little-endian array.
|
||||
pub fn to_bytes(&self) -> [u8; 64] {
|
||||
let mut buf = [0u8; 64];
|
||||
buf[0x00..0x04].copy_from_slice(&self.ebpf_magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.header_version.to_le_bytes());
|
||||
buf[0x06] = self.program_type;
|
||||
buf[0x07] = self.attach_type;
|
||||
buf[0x08..0x0C].copy_from_slice(&self.program_flags.to_le_bytes());
|
||||
buf[0x0C..0x0E].copy_from_slice(&self.insn_count.to_le_bytes());
|
||||
buf[0x0E..0x10].copy_from_slice(&self.max_dimension.to_le_bytes());
|
||||
buf[0x10..0x18].copy_from_slice(&self.program_size.to_le_bytes());
|
||||
buf[0x18..0x1C].copy_from_slice(&self.map_count.to_le_bytes());
|
||||
buf[0x1C..0x20].copy_from_slice(&self.btf_size.to_le_bytes());
|
||||
buf[0x20..0x40].copy_from_slice(&self.program_hash);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize an `EbpfHeader` from a 64-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 64]) -> Result<Self, RvfError> {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != EBPF_MAGIC {
|
||||
return Err(RvfError::BadMagic {
|
||||
expected: EBPF_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ebpf_magic: magic,
|
||||
header_version: u16::from_le_bytes([data[0x04], data[0x05]]),
|
||||
program_type: data[0x06],
|
||||
attach_type: data[0x07],
|
||||
program_flags: u32::from_le_bytes([data[0x08], data[0x09], data[0x0A], data[0x0B]]),
|
||||
insn_count: u16::from_le_bytes([data[0x0C], data[0x0D]]),
|
||||
max_dimension: u16::from_le_bytes([data[0x0E], data[0x0F]]),
|
||||
program_size: u64::from_le_bytes([
|
||||
data[0x10], data[0x11], data[0x12], data[0x13], data[0x14], data[0x15], data[0x16],
|
||||
data[0x17],
|
||||
]),
|
||||
map_count: u32::from_le_bytes([data[0x18], data[0x19], data[0x1A], data[0x1B]]),
|
||||
btf_size: u32::from_le_bytes([data[0x1C], data[0x1D], data[0x1E], data[0x1F]]),
|
||||
program_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x20..0x40]);
|
||||
h
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_header() -> EbpfHeader {
|
||||
EbpfHeader {
|
||||
ebpf_magic: EBPF_MAGIC,
|
||||
header_version: 1,
|
||||
program_type: EbpfProgramType::XdpDistance as u8,
|
||||
attach_type: EbpfAttachType::XdpIngress as u8,
|
||||
program_flags: 0,
|
||||
insn_count: 256,
|
||||
max_dimension: 1536,
|
||||
program_size: 4096,
|
||||
map_count: 2,
|
||||
btf_size: 512,
|
||||
program_hash: [0xDE; 32],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_64() {
|
||||
assert_eq!(core::mem::size_of::<EbpfHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
let bytes_be = EBPF_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVBP");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_header();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = EbpfHeader::from_bytes(&bytes).expect("from_bytes should succeed");
|
||||
|
||||
assert_eq!(decoded.ebpf_magic, EBPF_MAGIC);
|
||||
assert_eq!(decoded.header_version, 1);
|
||||
assert_eq!(decoded.program_type, EbpfProgramType::XdpDistance as u8);
|
||||
assert_eq!(decoded.attach_type, EbpfAttachType::XdpIngress as u8);
|
||||
assert_eq!(decoded.program_flags, 0);
|
||||
assert_eq!(decoded.insn_count, 256);
|
||||
assert_eq!(decoded.max_dimension, 1536);
|
||||
assert_eq!(decoded.program_size, 4096);
|
||||
assert_eq!(decoded.map_count, 2);
|
||||
assert_eq!(decoded.btf_size, 512);
|
||||
assert_eq!(decoded.program_hash, [0xDE; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_magic_returns_error() {
|
||||
let mut bytes = sample_header().to_bytes();
|
||||
bytes[0] = 0x00; // corrupt magic
|
||||
let err = EbpfHeader::from_bytes(&bytes).unwrap_err();
|
||||
match err {
|
||||
RvfError::BadMagic { expected, .. } => assert_eq!(expected, EBPF_MAGIC),
|
||||
other => panic!("expected BadMagic, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let h = sample_header();
|
||||
let base = &h as *const _ as usize;
|
||||
|
||||
assert_eq!(&h.ebpf_magic as *const _ as usize - base, 0x00);
|
||||
assert_eq!(&h.header_version as *const _ as usize - base, 0x04);
|
||||
assert_eq!(&h.program_type as *const _ as usize - base, 0x06);
|
||||
assert_eq!(&h.attach_type as *const _ as usize - base, 0x07);
|
||||
assert_eq!(&h.program_flags as *const _ as usize - base, 0x08);
|
||||
assert_eq!(&h.insn_count as *const _ as usize - base, 0x0C);
|
||||
assert_eq!(&h.max_dimension as *const _ as usize - base, 0x0E);
|
||||
assert_eq!(&h.program_size as *const _ as usize - base, 0x10);
|
||||
assert_eq!(&h.map_count as *const _ as usize - base, 0x18);
|
||||
assert_eq!(&h.btf_size as *const _ as usize - base, 0x1C);
|
||||
assert_eq!(&h.program_hash as *const _ as usize - base, 0x20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ebpf_program_type_try_from() {
|
||||
assert_eq!(
|
||||
EbpfProgramType::try_from(0x00),
|
||||
Ok(EbpfProgramType::XdpDistance)
|
||||
);
|
||||
assert_eq!(
|
||||
EbpfProgramType::try_from(0x01),
|
||||
Ok(EbpfProgramType::TcFilter)
|
||||
);
|
||||
assert_eq!(
|
||||
EbpfProgramType::try_from(0x02),
|
||||
Ok(EbpfProgramType::SocketFilter)
|
||||
);
|
||||
assert_eq!(
|
||||
EbpfProgramType::try_from(0x03),
|
||||
Ok(EbpfProgramType::Tracepoint)
|
||||
);
|
||||
assert_eq!(EbpfProgramType::try_from(0x04), Ok(EbpfProgramType::Kprobe));
|
||||
assert_eq!(
|
||||
EbpfProgramType::try_from(0x05),
|
||||
Ok(EbpfProgramType::CgroupSkb)
|
||||
);
|
||||
assert_eq!(EbpfProgramType::try_from(0xFF), Ok(EbpfProgramType::Custom));
|
||||
assert!(EbpfProgramType::try_from(0x06).is_err());
|
||||
assert!(EbpfProgramType::try_from(0x80).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ebpf_attach_type_try_from() {
|
||||
assert_eq!(
|
||||
EbpfAttachType::try_from(0x00),
|
||||
Ok(EbpfAttachType::XdpIngress)
|
||||
);
|
||||
assert_eq!(
|
||||
EbpfAttachType::try_from(0x01),
|
||||
Ok(EbpfAttachType::TcIngress)
|
||||
);
|
||||
assert_eq!(EbpfAttachType::try_from(0x02), Ok(EbpfAttachType::TcEgress));
|
||||
assert_eq!(
|
||||
EbpfAttachType::try_from(0x03),
|
||||
Ok(EbpfAttachType::SocketFilter)
|
||||
);
|
||||
assert_eq!(
|
||||
EbpfAttachType::try_from(0x04),
|
||||
Ok(EbpfAttachType::CgroupIngress)
|
||||
);
|
||||
assert_eq!(
|
||||
EbpfAttachType::try_from(0x05),
|
||||
Ok(EbpfAttachType::CgroupEgress)
|
||||
);
|
||||
assert_eq!(EbpfAttachType::try_from(0xFF), Ok(EbpfAttachType::None));
|
||||
assert!(EbpfAttachType::try_from(0x06).is_err());
|
||||
assert!(EbpfAttachType::try_from(0x80).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_dimension_round_trip() {
|
||||
let mut h = sample_header();
|
||||
h.max_dimension = 2048;
|
||||
let bytes = h.to_bytes();
|
||||
let decoded = EbpfHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.max_dimension, 2048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_program_size_round_trip() {
|
||||
let mut h = sample_header();
|
||||
h.program_size = 1_048_576; // 1 MiB
|
||||
h.insn_count = 65535;
|
||||
let bytes = h.to_bytes();
|
||||
let decoded = EbpfHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.program_size, 1_048_576);
|
||||
assert_eq!(decoded.insn_count, 65535);
|
||||
}
|
||||
}
|
||||
269
crates/rvf/rvf-types/src/ed25519.rs
Normal file
269
crates/rvf/rvf-types/src/ed25519.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
//! Ed25519 asymmetric signing (RFC 8032).
|
||||
//!
|
||||
//! Provides keypair generation, signing, and verification using the
|
||||
//! `ed25519-dalek` crate. Feature-gated behind the `ed25519` feature.
|
||||
|
||||
use ed25519_dalek::{Signature as DalekSignature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
|
||||
/// Ed25519 public key size in bytes.
|
||||
pub const PUBLIC_KEY_SIZE: usize = 32;
|
||||
|
||||
/// Ed25519 secret (signing) key size in bytes.
|
||||
pub const SECRET_KEY_SIZE: usize = 32;
|
||||
|
||||
/// Ed25519 signature size in bytes.
|
||||
pub const SIGNATURE_SIZE: usize = 64;
|
||||
|
||||
// Compile-time size assertions (mirrors sha256.rs pattern).
|
||||
const _: () = assert!(PUBLIC_KEY_SIZE == 32);
|
||||
const _: () = assert!(SECRET_KEY_SIZE == 32);
|
||||
const _: () = assert!(SIGNATURE_SIZE == 64);
|
||||
|
||||
/// An Ed25519 keypair (signing key + verifying key).
|
||||
///
|
||||
/// The signing key is 32 bytes of secret material; the verifying key
|
||||
/// is the corresponding 32-byte public point on the Ed25519 curve.
|
||||
#[derive(Clone)]
|
||||
pub struct Ed25519Keypair {
|
||||
signing: SigningKey,
|
||||
}
|
||||
|
||||
impl Ed25519Keypair {
|
||||
/// Generate a new random keypair from the provided RNG.
|
||||
pub fn generate<R: rand_core::CryptoRngCore>(rng: &mut R) -> Self {
|
||||
Self {
|
||||
signing: SigningKey::generate(rng),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct a keypair from a 32-byte secret key.
|
||||
pub fn from_secret(secret: &[u8; SECRET_KEY_SIZE]) -> Self {
|
||||
Self {
|
||||
signing: SigningKey::from_bytes(secret),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the 32-byte secret (signing) key.
|
||||
pub fn secret_key(&self) -> [u8; SECRET_KEY_SIZE] {
|
||||
self.signing.to_bytes()
|
||||
}
|
||||
|
||||
/// Return the 32-byte public (verifying) key.
|
||||
pub fn public_key(&self) -> [u8; PUBLIC_KEY_SIZE] {
|
||||
self.signing.verifying_key().to_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign `message` with an Ed25519 secret key. Returns a 64-byte signature.
|
||||
///
|
||||
/// This is a deterministic operation: the same key + message always
|
||||
/// produces the same signature (per RFC 8032).
|
||||
pub fn ed25519_sign(secret: &[u8; SECRET_KEY_SIZE], message: &[u8]) -> [u8; SIGNATURE_SIZE] {
|
||||
let signing_key = SigningKey::from_bytes(secret);
|
||||
let sig: DalekSignature = signing_key.sign(message);
|
||||
sig.to_bytes()
|
||||
}
|
||||
|
||||
/// Verify an Ed25519 signature against a public key and message.
|
||||
///
|
||||
/// Returns `true` if the signature is valid, `false` otherwise.
|
||||
pub fn ed25519_verify(
|
||||
public: &[u8; PUBLIC_KEY_SIZE],
|
||||
message: &[u8],
|
||||
signature: &[u8; SIGNATURE_SIZE],
|
||||
) -> bool {
|
||||
let Ok(verifying_key) = VerifyingKey::from_bytes(public) else {
|
||||
return false;
|
||||
};
|
||||
let sig = DalekSignature::from_bytes(signature);
|
||||
verifying_key.verify(message, &sig).is_ok()
|
||||
}
|
||||
|
||||
/// Constant-time comparison of two 64-byte signatures.
|
||||
pub fn ct_eq_sig(a: &[u8; SIGNATURE_SIZE], b: &[u8; SIGNATURE_SIZE]) -> bool {
|
||||
let mut diff = 0u8;
|
||||
for i in 0..SIGNATURE_SIZE {
|
||||
diff |= a[i] ^ b[i];
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Deterministic RNG for reproducible tests.
|
||||
struct TestRng(u64);
|
||||
|
||||
impl rand_core::RngCore for TestRng {
|
||||
fn next_u32(&mut self) -> u32 {
|
||||
self.next_u64() as u32
|
||||
}
|
||||
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
// Simple xorshift64 for test determinism.
|
||||
self.0 ^= self.0 << 13;
|
||||
self.0 ^= self.0 >> 7;
|
||||
self.0 ^= self.0 << 17;
|
||||
self.0
|
||||
}
|
||||
|
||||
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||
let mut i = 0;
|
||||
while i < dest.len() {
|
||||
let val = self.next_u64().to_le_bytes();
|
||||
let remaining = dest.len() - i;
|
||||
let take = if remaining < 8 { remaining } else { 8 };
|
||||
dest[i..i + take].copy_from_slice(&val[..take]);
|
||||
i += take;
|
||||
}
|
||||
}
|
||||
|
||||
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
|
||||
self.fill_bytes(dest);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl rand_core::CryptoRng for TestRng {}
|
||||
|
||||
fn test_rng() -> TestRng {
|
||||
TestRng(0xDEAD_BEEF_CAFE_1234)
|
||||
}
|
||||
|
||||
// --- Test 1: keypair generation ---
|
||||
|
||||
#[test]
|
||||
fn keygen_produces_valid_keypair() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
|
||||
let secret = kp.secret_key();
|
||||
let public = kp.public_key();
|
||||
|
||||
assert_eq!(secret.len(), SECRET_KEY_SIZE);
|
||||
assert_eq!(public.len(), PUBLIC_KEY_SIZE);
|
||||
// Keys should not be all zeros.
|
||||
assert_ne!(secret, [0u8; SECRET_KEY_SIZE]);
|
||||
assert_ne!(public, [0u8; PUBLIC_KEY_SIZE]);
|
||||
}
|
||||
|
||||
// --- Test 2: sign produces a signature ---
|
||||
|
||||
#[test]
|
||||
fn sign_returns_64_byte_signature() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
let message = b"hello RVF";
|
||||
|
||||
let sig = ed25519_sign(&kp.secret_key(), message);
|
||||
assert_eq!(sig.len(), SIGNATURE_SIZE);
|
||||
assert_ne!(sig, [0u8; SIGNATURE_SIZE]);
|
||||
}
|
||||
|
||||
// --- Test 3: sign then verify round-trip ---
|
||||
|
||||
#[test]
|
||||
fn sign_verify_round_trip() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
let message = b"The quick brown fox jumps over the lazy dog";
|
||||
|
||||
let sig = ed25519_sign(&kp.secret_key(), message);
|
||||
assert!(ed25519_verify(&kp.public_key(), message, &sig));
|
||||
}
|
||||
|
||||
// --- Test 4: wrong key rejects ---
|
||||
|
||||
#[test]
|
||||
fn wrong_key_rejects() {
|
||||
let mut rng = test_rng();
|
||||
let kp1 = Ed25519Keypair::generate(&mut rng);
|
||||
let kp2 = Ed25519Keypair::generate(&mut rng);
|
||||
let message = b"signed by kp1";
|
||||
|
||||
let sig = ed25519_sign(&kp1.secret_key(), message);
|
||||
// Verify with kp2's public key should fail.
|
||||
assert!(!ed25519_verify(&kp2.public_key(), message, &sig));
|
||||
}
|
||||
|
||||
// --- Test 5: tampered message rejects ---
|
||||
|
||||
#[test]
|
||||
fn tampered_message_rejects() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
let message = b"original payload";
|
||||
|
||||
let sig = ed25519_sign(&kp.secret_key(), message);
|
||||
assert!(!ed25519_verify(&kp.public_key(), b"tampered payload", &sig));
|
||||
}
|
||||
|
||||
// --- Test 6: deterministic signatures ---
|
||||
|
||||
#[test]
|
||||
fn deterministic_signatures() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
let message = b"determinism test";
|
||||
|
||||
let sig1 = ed25519_sign(&kp.secret_key(), message);
|
||||
let sig2 = ed25519_sign(&kp.secret_key(), message);
|
||||
assert_eq!(sig1, sig2);
|
||||
}
|
||||
|
||||
// --- Test 7: different messages produce different signatures ---
|
||||
|
||||
#[test]
|
||||
fn different_messages_different_sigs() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
|
||||
let sig_a = ed25519_sign(&kp.secret_key(), b"message A");
|
||||
let sig_b = ed25519_sign(&kp.secret_key(), b"message B");
|
||||
assert_ne!(sig_a, sig_b);
|
||||
}
|
||||
|
||||
// --- Test 8: empty message ---
|
||||
|
||||
#[test]
|
||||
fn empty_message_sign_verify() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
let message = b"";
|
||||
|
||||
let sig = ed25519_sign(&kp.secret_key(), message);
|
||||
assert!(ed25519_verify(&kp.public_key(), message, &sig));
|
||||
}
|
||||
|
||||
// --- Additional tests ---
|
||||
|
||||
#[test]
|
||||
fn from_secret_round_trip() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
|
||||
let secret = kp.secret_key();
|
||||
let restored = Ed25519Keypair::from_secret(&secret);
|
||||
|
||||
assert_eq!(kp.public_key(), restored.public_key());
|
||||
assert_eq!(kp.secret_key(), restored.secret_key());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_sig_same() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
let sig = ed25519_sign(&kp.secret_key(), b"test");
|
||||
assert!(ct_eq_sig(&sig, &sig));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_sig_different() {
|
||||
let mut rng = test_rng();
|
||||
let kp = Ed25519Keypair::generate(&mut rng);
|
||||
let sig1 = ed25519_sign(&kp.secret_key(), b"msg1");
|
||||
let sig2 = ed25519_sign(&kp.secret_key(), b"msg2");
|
||||
assert!(!ct_eq_sig(&sig1, &sig2));
|
||||
}
|
||||
}
|
||||
453
crates/rvf/rvf-types/src/error.rs
Normal file
453
crates/rvf/rvf-types/src/error.rs
Normal file
@@ -0,0 +1,453 @@
|
||||
//! Error codes and error types for the RVF format.
|
||||
//!
|
||||
//! Error codes are 16-bit unsigned integers where the high byte identifies
|
||||
//! the category and the low byte the specific error.
|
||||
|
||||
/// Wire-format error code (u16). The high byte is the category, the low byte is
|
||||
/// the specific error within that category.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u16)]
|
||||
pub enum ErrorCode {
|
||||
// ---- Category 0x00: Success ----
|
||||
/// Operation succeeded.
|
||||
Ok = 0x0000,
|
||||
/// Partial success (some items failed).
|
||||
OkPartial = 0x0001,
|
||||
|
||||
// ---- Category 0x01: Format Errors ----
|
||||
/// Segment magic mismatch (expected 0x52564653).
|
||||
InvalidMagic = 0x0100,
|
||||
/// Unsupported segment version.
|
||||
InvalidVersion = 0x0101,
|
||||
/// Segment hash verification failed.
|
||||
InvalidChecksum = 0x0102,
|
||||
/// Cryptographic signature invalid.
|
||||
InvalidSignature = 0x0103,
|
||||
/// Segment payload shorter than declared length.
|
||||
TruncatedSegment = 0x0104,
|
||||
/// Root manifest validation failed.
|
||||
InvalidManifest = 0x0105,
|
||||
/// No valid MANIFEST_SEG in file.
|
||||
ManifestNotFound = 0x0106,
|
||||
/// Segment type not recognized (advisory, not fatal).
|
||||
UnknownSegmentType = 0x0107,
|
||||
/// Data not at expected 64-byte boundary.
|
||||
AlignmentError = 0x0108,
|
||||
|
||||
// ---- Category 0x02: Query Errors ----
|
||||
/// Query vector dimension != index dimension.
|
||||
DimensionMismatch = 0x0200,
|
||||
/// No index segments available.
|
||||
EmptyIndex = 0x0201,
|
||||
/// Requested distance metric not available.
|
||||
MetricUnsupported = 0x0202,
|
||||
/// Invalid filter expression.
|
||||
FilterParseError = 0x0203,
|
||||
/// Requested K exceeds available vectors.
|
||||
KTooLarge = 0x0204,
|
||||
/// Query exceeded time budget.
|
||||
Timeout = 0x0205,
|
||||
|
||||
// ---- Category 0x03: Write Errors ----
|
||||
/// Another writer holds the lock.
|
||||
LockHeld = 0x0300,
|
||||
/// Lock file exists but owner process is dead.
|
||||
LockStale = 0x0301,
|
||||
/// Insufficient space for write.
|
||||
DiskFull = 0x0302,
|
||||
/// Durable write (fsync) failed.
|
||||
FsyncFailed = 0x0303,
|
||||
/// Segment exceeds 4 GB limit.
|
||||
SegmentTooLarge = 0x0304,
|
||||
/// File opened in read-only mode.
|
||||
ReadOnly = 0x0305,
|
||||
|
||||
// ---- Category 0x04: Tile Errors (WASM Microkernel) ----
|
||||
/// WASM trap (OOB, unreachable, stack overflow).
|
||||
TileTrap = 0x0400,
|
||||
/// Tile exceeded scratch memory (64 KB).
|
||||
TileOom = 0x0401,
|
||||
/// Tile computation exceeded time budget.
|
||||
TileTimeout = 0x0402,
|
||||
/// Malformed hub-tile message.
|
||||
TileInvalidMsg = 0x0403,
|
||||
/// Operation not available on this profile.
|
||||
TileUnsupportedOp = 0x0404,
|
||||
|
||||
// ---- Category 0x05: Crypto Errors ----
|
||||
/// Referenced key_id not in CRYPTO_SEG.
|
||||
KeyNotFound = 0x0500,
|
||||
/// Key past valid_until timestamp.
|
||||
KeyExpired = 0x0501,
|
||||
/// Decryption or auth tag verification failed.
|
||||
DecryptFailed = 0x0502,
|
||||
/// Cryptographic algorithm not implemented.
|
||||
AlgoUnsupported = 0x0503,
|
||||
/// Attestation quote verification failed.
|
||||
AttestationInvalid = 0x0504,
|
||||
/// TEE platform not supported.
|
||||
PlatformUnsupported = 0x0505,
|
||||
/// Attestation quote expired or nonce mismatch.
|
||||
AttestationExpired = 0x0506,
|
||||
/// Key is not bound to the current TEE measurement.
|
||||
KeyNotBound = 0x0507,
|
||||
|
||||
// ---- Category 0x06: Lineage Errors ----
|
||||
/// Referenced parent file not found.
|
||||
ParentNotFound = 0x0600,
|
||||
/// Parent file hash does not match recorded parent_hash.
|
||||
ParentHashMismatch = 0x0601,
|
||||
/// Lineage chain is broken (missing link).
|
||||
LineageBroken = 0x0602,
|
||||
/// Lineage chain contains a cycle.
|
||||
LineageCyclic = 0x0603,
|
||||
|
||||
// ---- Category 0x08: Security Errors (ADR-033) ----
|
||||
/// Level 0 manifest has no signature in Strict/Paranoid mode.
|
||||
UnsignedManifest = 0x0800,
|
||||
/// Content hash mismatch on a hotset-referenced segment.
|
||||
ContentHashMismatch = 0x0801,
|
||||
/// Manifest signer is not in the trust store.
|
||||
UnknownSigner = 0x0802,
|
||||
/// Centroid epoch drift exceeds maximum allowed.
|
||||
EpochDriftExceeded = 0x0803,
|
||||
/// Level 1 manifest signature invalid (Paranoid mode).
|
||||
Level1InvalidSignature = 0x0804,
|
||||
|
||||
// ---- Category 0x09: Quality Errors (ADR-033) ----
|
||||
/// Query result quality is below threshold and AcceptDegraded not set.
|
||||
QualityBelowThreshold = 0x0900,
|
||||
/// Per-connection budget tokens exhausted (DoS protection).
|
||||
BudgetTokensExhausted = 0x0901,
|
||||
/// Query signature is blacklisted (repeated degenerate queries).
|
||||
QueryBlacklisted = 0x0902,
|
||||
|
||||
// ---- Category 0x07: COW Errors ----
|
||||
/// COW cluster map is corrupt or unreadable.
|
||||
CowMapCorrupt = 0x0700,
|
||||
/// Referenced cluster not found in COW map.
|
||||
ClusterNotFound = 0x0701,
|
||||
/// Parent chain is broken (missing ancestor).
|
||||
ParentChainBroken = 0x0702,
|
||||
/// Delta patch exceeds compaction threshold.
|
||||
DeltaThresholdExceeded = 0x0703,
|
||||
/// Snapshot is frozen and cannot be modified.
|
||||
SnapshotFrozen = 0x0704,
|
||||
/// Membership filter is invalid or corrupt.
|
||||
MembershipInvalid = 0x0705,
|
||||
/// Generation counter is stale (concurrent modification).
|
||||
GenerationStale = 0x0706,
|
||||
/// Kernel binding hash does not match manifest.
|
||||
KernelBindingMismatch = 0x0707,
|
||||
/// Double-root manifest is corrupt.
|
||||
DoubleRootCorrupt = 0x0708,
|
||||
}
|
||||
|
||||
impl ErrorCode {
|
||||
/// Return the error category (high byte).
|
||||
#[inline]
|
||||
pub const fn category(self) -> u8 {
|
||||
(self as u16 >> 8) as u8
|
||||
}
|
||||
|
||||
/// Return true if this code indicates success (category 0x00).
|
||||
#[inline]
|
||||
pub const fn is_success(self) -> bool {
|
||||
self.category() == 0x00
|
||||
}
|
||||
|
||||
/// Return true if this is a format error (category 0x01), which is generally fatal.
|
||||
#[inline]
|
||||
pub const fn is_format_error(self) -> bool {
|
||||
self.category() == 0x01
|
||||
}
|
||||
|
||||
/// Return true if this is a security error (category 0x08).
|
||||
#[inline]
|
||||
pub const fn is_security_error(self) -> bool {
|
||||
self.category() == 0x08
|
||||
}
|
||||
|
||||
/// Return true if this is a quality error (category 0x09).
|
||||
#[inline]
|
||||
pub const fn is_quality_error(self) -> bool {
|
||||
self.category() == 0x09
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for ErrorCode {
|
||||
type Error = u16;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x0000 => Ok(Self::Ok),
|
||||
0x0001 => Ok(Self::OkPartial),
|
||||
|
||||
0x0100 => Ok(Self::InvalidMagic),
|
||||
0x0101 => Ok(Self::InvalidVersion),
|
||||
0x0102 => Ok(Self::InvalidChecksum),
|
||||
0x0103 => Ok(Self::InvalidSignature),
|
||||
0x0104 => Ok(Self::TruncatedSegment),
|
||||
0x0105 => Ok(Self::InvalidManifest),
|
||||
0x0106 => Ok(Self::ManifestNotFound),
|
||||
0x0107 => Ok(Self::UnknownSegmentType),
|
||||
0x0108 => Ok(Self::AlignmentError),
|
||||
|
||||
0x0200 => Ok(Self::DimensionMismatch),
|
||||
0x0201 => Ok(Self::EmptyIndex),
|
||||
0x0202 => Ok(Self::MetricUnsupported),
|
||||
0x0203 => Ok(Self::FilterParseError),
|
||||
0x0204 => Ok(Self::KTooLarge),
|
||||
0x0205 => Ok(Self::Timeout),
|
||||
|
||||
0x0300 => Ok(Self::LockHeld),
|
||||
0x0301 => Ok(Self::LockStale),
|
||||
0x0302 => Ok(Self::DiskFull),
|
||||
0x0303 => Ok(Self::FsyncFailed),
|
||||
0x0304 => Ok(Self::SegmentTooLarge),
|
||||
0x0305 => Ok(Self::ReadOnly),
|
||||
|
||||
0x0400 => Ok(Self::TileTrap),
|
||||
0x0401 => Ok(Self::TileOom),
|
||||
0x0402 => Ok(Self::TileTimeout),
|
||||
0x0403 => Ok(Self::TileInvalidMsg),
|
||||
0x0404 => Ok(Self::TileUnsupportedOp),
|
||||
|
||||
0x0500 => Ok(Self::KeyNotFound),
|
||||
0x0501 => Ok(Self::KeyExpired),
|
||||
0x0502 => Ok(Self::DecryptFailed),
|
||||
0x0503 => Ok(Self::AlgoUnsupported),
|
||||
0x0504 => Ok(Self::AttestationInvalid),
|
||||
0x0505 => Ok(Self::PlatformUnsupported),
|
||||
0x0506 => Ok(Self::AttestationExpired),
|
||||
0x0507 => Ok(Self::KeyNotBound),
|
||||
|
||||
0x0600 => Ok(Self::ParentNotFound),
|
||||
0x0601 => Ok(Self::ParentHashMismatch),
|
||||
0x0602 => Ok(Self::LineageBroken),
|
||||
0x0603 => Ok(Self::LineageCyclic),
|
||||
|
||||
0x0800 => Ok(Self::UnsignedManifest),
|
||||
0x0801 => Ok(Self::ContentHashMismatch),
|
||||
0x0802 => Ok(Self::UnknownSigner),
|
||||
0x0803 => Ok(Self::EpochDriftExceeded),
|
||||
0x0804 => Ok(Self::Level1InvalidSignature),
|
||||
|
||||
0x0900 => Ok(Self::QualityBelowThreshold),
|
||||
0x0901 => Ok(Self::BudgetTokensExhausted),
|
||||
0x0902 => Ok(Self::QueryBlacklisted),
|
||||
|
||||
0x0700 => Ok(Self::CowMapCorrupt),
|
||||
0x0701 => Ok(Self::ClusterNotFound),
|
||||
0x0702 => Ok(Self::ParentChainBroken),
|
||||
0x0703 => Ok(Self::DeltaThresholdExceeded),
|
||||
0x0704 => Ok(Self::SnapshotFrozen),
|
||||
0x0705 => Ok(Self::MembershipInvalid),
|
||||
0x0706 => Ok(Self::GenerationStale),
|
||||
0x0707 => Ok(Self::KernelBindingMismatch),
|
||||
0x0708 => Ok(Self::DoubleRootCorrupt),
|
||||
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rust-idiomatic error type wrapping format-level failures.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum RvfError {
|
||||
/// A wire-level error code was returned.
|
||||
Code(ErrorCode),
|
||||
/// The raw u16 did not map to a known error code.
|
||||
UnknownCode(u16),
|
||||
/// A segment header had an invalid magic number.
|
||||
BadMagic { expected: u32, got: u32 },
|
||||
/// A struct size assertion failed.
|
||||
SizeMismatch { expected: usize, got: usize },
|
||||
/// A value was outside the valid enum range.
|
||||
InvalidEnumValue { type_name: &'static str, value: u64 },
|
||||
/// Security policy violation during file open (ADR-033 §4).
|
||||
Security(crate::security::SecurityError),
|
||||
/// Query result quality is below threshold (ADR-033 §2.4).
|
||||
/// Contains the QualityEnvelope with partial results and diagnostics.
|
||||
QualityBelowThreshold {
|
||||
quality: crate::quality::ResponseQuality,
|
||||
reason: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RvfError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Code(c) => write!(f, "RVF error code 0x{:04X}", *c as u16),
|
||||
Self::UnknownCode(v) => write!(f, "unknown RVF error code 0x{v:04X}"),
|
||||
Self::BadMagic { expected, got } => {
|
||||
write!(f, "bad magic: expected 0x{expected:08X}, got 0x{got:08X}")
|
||||
}
|
||||
Self::SizeMismatch { expected, got } => {
|
||||
write!(f, "size mismatch: expected {expected}, got {got}")
|
||||
}
|
||||
Self::InvalidEnumValue { type_name, value } => {
|
||||
write!(f, "invalid {type_name} value: {value}")
|
||||
}
|
||||
Self::Security(e) => write!(f, "security error: {e}"),
|
||||
Self::QualityBelowThreshold { quality, reason } => {
|
||||
write!(f, "quality below threshold ({quality:?}): {reason}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::format;
|
||||
|
||||
#[test]
|
||||
fn error_code_round_trip_all() {
|
||||
let codes: &[(u16, ErrorCode)] = &[
|
||||
(0x0000, ErrorCode::Ok),
|
||||
(0x0001, ErrorCode::OkPartial),
|
||||
(0x0100, ErrorCode::InvalidMagic),
|
||||
(0x0101, ErrorCode::InvalidVersion),
|
||||
(0x0102, ErrorCode::InvalidChecksum),
|
||||
(0x0103, ErrorCode::InvalidSignature),
|
||||
(0x0104, ErrorCode::TruncatedSegment),
|
||||
(0x0105, ErrorCode::InvalidManifest),
|
||||
(0x0106, ErrorCode::ManifestNotFound),
|
||||
(0x0107, ErrorCode::UnknownSegmentType),
|
||||
(0x0108, ErrorCode::AlignmentError),
|
||||
(0x0200, ErrorCode::DimensionMismatch),
|
||||
(0x0201, ErrorCode::EmptyIndex),
|
||||
(0x0202, ErrorCode::MetricUnsupported),
|
||||
(0x0203, ErrorCode::FilterParseError),
|
||||
(0x0204, ErrorCode::KTooLarge),
|
||||
(0x0205, ErrorCode::Timeout),
|
||||
(0x0300, ErrorCode::LockHeld),
|
||||
(0x0301, ErrorCode::LockStale),
|
||||
(0x0302, ErrorCode::DiskFull),
|
||||
(0x0303, ErrorCode::FsyncFailed),
|
||||
(0x0304, ErrorCode::SegmentTooLarge),
|
||||
(0x0305, ErrorCode::ReadOnly),
|
||||
(0x0400, ErrorCode::TileTrap),
|
||||
(0x0401, ErrorCode::TileOom),
|
||||
(0x0402, ErrorCode::TileTimeout),
|
||||
(0x0403, ErrorCode::TileInvalidMsg),
|
||||
(0x0404, ErrorCode::TileUnsupportedOp),
|
||||
(0x0500, ErrorCode::KeyNotFound),
|
||||
(0x0501, ErrorCode::KeyExpired),
|
||||
(0x0502, ErrorCode::DecryptFailed),
|
||||
(0x0503, ErrorCode::AlgoUnsupported),
|
||||
(0x0504, ErrorCode::AttestationInvalid),
|
||||
(0x0505, ErrorCode::PlatformUnsupported),
|
||||
(0x0506, ErrorCode::AttestationExpired),
|
||||
(0x0507, ErrorCode::KeyNotBound),
|
||||
(0x0600, ErrorCode::ParentNotFound),
|
||||
(0x0601, ErrorCode::ParentHashMismatch),
|
||||
(0x0602, ErrorCode::LineageBroken),
|
||||
(0x0603, ErrorCode::LineageCyclic),
|
||||
(0x0800, ErrorCode::UnsignedManifest),
|
||||
(0x0801, ErrorCode::ContentHashMismatch),
|
||||
(0x0802, ErrorCode::UnknownSigner),
|
||||
(0x0803, ErrorCode::EpochDriftExceeded),
|
||||
(0x0804, ErrorCode::Level1InvalidSignature),
|
||||
(0x0900, ErrorCode::QualityBelowThreshold),
|
||||
(0x0901, ErrorCode::BudgetTokensExhausted),
|
||||
(0x0902, ErrorCode::QueryBlacklisted),
|
||||
(0x0700, ErrorCode::CowMapCorrupt),
|
||||
(0x0701, ErrorCode::ClusterNotFound),
|
||||
(0x0702, ErrorCode::ParentChainBroken),
|
||||
(0x0703, ErrorCode::DeltaThresholdExceeded),
|
||||
(0x0704, ErrorCode::SnapshotFrozen),
|
||||
(0x0705, ErrorCode::MembershipInvalid),
|
||||
(0x0706, ErrorCode::GenerationStale),
|
||||
(0x0707, ErrorCode::KernelBindingMismatch),
|
||||
(0x0708, ErrorCode::DoubleRootCorrupt),
|
||||
];
|
||||
for &(raw, expected) in codes {
|
||||
assert_eq!(ErrorCode::try_from(raw), Ok(expected), "code 0x{raw:04X}");
|
||||
assert_eq!(expected as u16, raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_code() {
|
||||
assert_eq!(ErrorCode::try_from(0x9999), Err(0x9999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_extraction() {
|
||||
assert_eq!(ErrorCode::Ok.category(), 0x00);
|
||||
assert_eq!(ErrorCode::InvalidMagic.category(), 0x01);
|
||||
assert_eq!(ErrorCode::DimensionMismatch.category(), 0x02);
|
||||
assert_eq!(ErrorCode::LockHeld.category(), 0x03);
|
||||
assert_eq!(ErrorCode::TileTrap.category(), 0x04);
|
||||
assert_eq!(ErrorCode::KeyNotFound.category(), 0x05);
|
||||
assert_eq!(ErrorCode::ParentNotFound.category(), 0x06);
|
||||
assert_eq!(ErrorCode::CowMapCorrupt.category(), 0x07);
|
||||
assert_eq!(ErrorCode::UnsignedManifest.category(), 0x08);
|
||||
assert_eq!(ErrorCode::QualityBelowThreshold.category(), 0x09);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_error_check() {
|
||||
assert!(ErrorCode::UnsignedManifest.is_security_error());
|
||||
assert!(!ErrorCode::Ok.is_security_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_error_check() {
|
||||
assert!(ErrorCode::QualityBelowThreshold.is_quality_error());
|
||||
assert!(!ErrorCode::Ok.is_quality_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_check() {
|
||||
assert!(ErrorCode::Ok.is_success());
|
||||
assert!(ErrorCode::OkPartial.is_success());
|
||||
assert!(!ErrorCode::InvalidMagic.is_success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_error_check() {
|
||||
assert!(ErrorCode::InvalidMagic.is_format_error());
|
||||
assert!(!ErrorCode::Ok.is_format_error());
|
||||
assert!(!ErrorCode::DimensionMismatch.is_format_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rvf_error_display() {
|
||||
let e = RvfError::BadMagic {
|
||||
expected: 0x52564653,
|
||||
got: 0x00000000,
|
||||
};
|
||||
let s = format!("{e}");
|
||||
assert!(s.contains("bad magic"));
|
||||
assert!(s.contains("52564653"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cow_error_check() {
|
||||
assert_eq!(ErrorCode::CowMapCorrupt as u16, 0x0700);
|
||||
assert_eq!(ErrorCode::ClusterNotFound as u16, 0x0701);
|
||||
assert_eq!(ErrorCode::ParentChainBroken as u16, 0x0702);
|
||||
assert_eq!(ErrorCode::DeltaThresholdExceeded as u16, 0x0703);
|
||||
assert_eq!(ErrorCode::SnapshotFrozen as u16, 0x0704);
|
||||
assert_eq!(ErrorCode::MembershipInvalid as u16, 0x0705);
|
||||
assert_eq!(ErrorCode::GenerationStale as u16, 0x0706);
|
||||
assert_eq!(ErrorCode::KernelBindingMismatch as u16, 0x0707);
|
||||
assert_eq!(ErrorCode::DoubleRootCorrupt as u16, 0x0708);
|
||||
// All COW errors should be category 0x07
|
||||
assert_eq!(ErrorCode::CowMapCorrupt.category(), 0x07);
|
||||
assert_eq!(ErrorCode::DoubleRootCorrupt.category(), 0x07);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_codes_match_spec() {
|
||||
assert_eq!(ErrorCode::InvalidMagic as u16, 0x0100);
|
||||
assert_eq!(ErrorCode::InvalidChecksum as u16, 0x0102);
|
||||
assert_eq!(ErrorCode::ManifestNotFound as u16, 0x0106);
|
||||
assert_eq!(ErrorCode::AlgoUnsupported as u16, 0x0503);
|
||||
}
|
||||
}
|
||||
100
crates/rvf/rvf-types/src/filter.rs
Normal file
100
crates/rvf/rvf-types/src/filter.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
//! Filter operator types for metadata-filtered queries and deletes.
|
||||
|
||||
/// Filter operator discriminator.
|
||||
///
|
||||
/// Comparison operators use the low nibble (0x00..0x07), logical combinators
|
||||
/// use the 0x10 range.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum FilterOp {
|
||||
/// field == value
|
||||
Eq = 0x00,
|
||||
/// field != value
|
||||
Ne = 0x01,
|
||||
/// field < value
|
||||
Lt = 0x02,
|
||||
/// field <= value
|
||||
Le = 0x03,
|
||||
/// field > value
|
||||
Gt = 0x04,
|
||||
/// field >= value
|
||||
Ge = 0x05,
|
||||
/// field in [values]
|
||||
In = 0x06,
|
||||
/// field in [low, high)
|
||||
Range = 0x07,
|
||||
/// All children must match.
|
||||
And = 0x10,
|
||||
/// Any child must match.
|
||||
Or = 0x11,
|
||||
/// Negate single child.
|
||||
Not = 0x12,
|
||||
}
|
||||
|
||||
impl FilterOp {
|
||||
/// Returns true if this is a logical combinator (AND, OR, NOT).
|
||||
#[inline]
|
||||
pub const fn is_logical(self) -> bool {
|
||||
(self as u8) >= 0x10
|
||||
}
|
||||
|
||||
/// Returns true if this is a comparison operator.
|
||||
#[inline]
|
||||
pub const fn is_comparison(self) -> bool {
|
||||
(self as u8) < 0x10
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for FilterOp {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::Eq),
|
||||
0x01 => Ok(Self::Ne),
|
||||
0x02 => Ok(Self::Lt),
|
||||
0x03 => Ok(Self::Le),
|
||||
0x04 => Ok(Self::Gt),
|
||||
0x05 => Ok(Self::Ge),
|
||||
0x06 => Ok(Self::In),
|
||||
0x07 => Ok(Self::Range),
|
||||
0x10 => Ok(Self::And),
|
||||
0x11 => Ok(Self::Or),
|
||||
0x12 => Ok(Self::Not),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip_comparison_ops() {
|
||||
for raw in 0x00..=0x07u8 {
|
||||
let op = FilterOp::try_from(raw).unwrap();
|
||||
assert_eq!(op as u8, raw);
|
||||
assert!(op.is_comparison());
|
||||
assert!(!op.is_logical());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_logical_ops() {
|
||||
for raw in [0x10u8, 0x11, 0x12] {
|
||||
let op = FilterOp::try_from(raw).unwrap();
|
||||
assert_eq!(op as u8, raw);
|
||||
assert!(op.is_logical());
|
||||
assert!(!op.is_comparison());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_values_are_invalid() {
|
||||
for raw in 0x08..=0x0Fu8 {
|
||||
assert_eq!(FilterOp::try_from(raw), Err(raw));
|
||||
}
|
||||
}
|
||||
}
|
||||
137
crates/rvf/rvf-types/src/flags.rs
Normal file
137
crates/rvf/rvf-types/src/flags.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
//! Segment flags bitfield for the RVF format.
|
||||
|
||||
/// Bitfield wrapper around the 16-bit segment flags.
|
||||
///
|
||||
/// Bits 12-15 are reserved and must be zero.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(transparent)]
|
||||
pub struct SegmentFlags(u16);
|
||||
|
||||
impl SegmentFlags {
|
||||
/// Payload is compressed per the `compression` header field.
|
||||
pub const COMPRESSED: u16 = 0x0001;
|
||||
/// Payload is encrypted (key info in CRYPTO_SEG / manifest).
|
||||
pub const ENCRYPTED: u16 = 0x0002;
|
||||
/// A signature footer follows the payload.
|
||||
pub const SIGNED: u16 = 0x0004;
|
||||
/// Segment is immutable (compaction output).
|
||||
pub const SEALED: u16 = 0x0008;
|
||||
/// Segment is a partial / streaming write.
|
||||
pub const PARTIAL: u16 = 0x0010;
|
||||
/// Segment logically deletes a prior segment.
|
||||
pub const TOMBSTONE: u16 = 0x0020;
|
||||
/// Segment contains temperature-promoted (hot) data.
|
||||
pub const HOT: u16 = 0x0040;
|
||||
/// Segment contains overlay / delta data.
|
||||
pub const OVERLAY: u16 = 0x0080;
|
||||
/// Segment contains a full snapshot (not delta).
|
||||
pub const SNAPSHOT: u16 = 0x0100;
|
||||
/// Segment is a safe rollback point.
|
||||
pub const CHECKPOINT: u16 = 0x0200;
|
||||
/// Segment was produced inside an attested TEE environment.
|
||||
pub const ATTESTED: u16 = 0x0400;
|
||||
/// File carries DNA-style lineage provenance metadata.
|
||||
pub const HAS_LINEAGE: u16 = 0x0800;
|
||||
|
||||
/// Mask for all defined flag bits.
|
||||
const KNOWN_MASK: u16 = 0x0FFF;
|
||||
|
||||
/// Create an empty flags value (no flags set).
|
||||
#[inline]
|
||||
pub const fn empty() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
/// Create flags from a raw `u16`. Reserved bits are masked off.
|
||||
#[inline]
|
||||
pub const fn from_raw(raw: u16) -> Self {
|
||||
Self(raw & Self::KNOWN_MASK)
|
||||
}
|
||||
|
||||
/// Return the raw `u16` representation.
|
||||
#[inline]
|
||||
pub const fn bits(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Check whether a specific flag bit is set.
|
||||
#[inline]
|
||||
pub const fn contains(self, flag: u16) -> bool {
|
||||
self.0 & flag == flag
|
||||
}
|
||||
|
||||
/// Set a flag bit.
|
||||
#[inline]
|
||||
pub const fn with(self, flag: u16) -> Self {
|
||||
Self(self.0 | (flag & Self::KNOWN_MASK))
|
||||
}
|
||||
|
||||
/// Clear a flag bit.
|
||||
#[inline]
|
||||
pub const fn without(self, flag: u16) -> Self {
|
||||
Self(self.0 & !flag)
|
||||
}
|
||||
|
||||
/// Returns true if no flags are set.
|
||||
#[inline]
|
||||
pub const fn is_empty(self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_flags() {
|
||||
let f = SegmentFlags::empty();
|
||||
assert!(f.is_empty());
|
||||
assert_eq!(f.bits(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_check_flags() {
|
||||
let f = SegmentFlags::empty()
|
||||
.with(SegmentFlags::COMPRESSED)
|
||||
.with(SegmentFlags::SEALED);
|
||||
assert!(f.contains(SegmentFlags::COMPRESSED));
|
||||
assert!(f.contains(SegmentFlags::SEALED));
|
||||
assert!(!f.contains(SegmentFlags::ENCRYPTED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_flag() {
|
||||
let f = SegmentFlags::empty()
|
||||
.with(SegmentFlags::COMPRESSED)
|
||||
.with(SegmentFlags::SIGNED)
|
||||
.without(SegmentFlags::COMPRESSED);
|
||||
assert!(!f.contains(SegmentFlags::COMPRESSED));
|
||||
assert!(f.contains(SegmentFlags::SIGNED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_bits_masked() {
|
||||
let f = SegmentFlags::from_raw(0xFFFF);
|
||||
assert_eq!(f.bits(), 0x0FFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_known_flags() {
|
||||
let all = SegmentFlags::empty()
|
||||
.with(SegmentFlags::COMPRESSED)
|
||||
.with(SegmentFlags::ENCRYPTED)
|
||||
.with(SegmentFlags::SIGNED)
|
||||
.with(SegmentFlags::SEALED)
|
||||
.with(SegmentFlags::PARTIAL)
|
||||
.with(SegmentFlags::TOMBSTONE)
|
||||
.with(SegmentFlags::HOT)
|
||||
.with(SegmentFlags::OVERLAY)
|
||||
.with(SegmentFlags::SNAPSHOT)
|
||||
.with(SegmentFlags::CHECKPOINT)
|
||||
.with(SegmentFlags::ATTESTED)
|
||||
.with(SegmentFlags::HAS_LINEAGE);
|
||||
assert_eq!(all.bits(), 0x0FFF);
|
||||
}
|
||||
}
|
||||
479
crates/rvf/rvf-types/src/kernel.rs
Normal file
479
crates/rvf/rvf-types/src/kernel.rs
Normal file
@@ -0,0 +1,479 @@
|
||||
//! KERNEL_SEG (0x0E) types for the RVF computational container.
|
||||
//!
|
||||
//! Defines the 128-byte `KernelHeader` and associated enums per ADR-030.
|
||||
//! The KERNEL_SEG embeds a unikernel image that can self-boot an RVF file
|
||||
//! as a standalone query-serving microservice.
|
||||
|
||||
use crate::error::RvfError;
|
||||
|
||||
/// Magic number for `KernelHeader`: "RVKN" in big-endian.
|
||||
pub const KERNEL_MAGIC: u32 = 0x5256_4B4E;
|
||||
|
||||
/// Kernel flags: kernel image is cryptographically signed.
|
||||
pub const KERNEL_FLAG_SIGNED: u32 = 1 << 8;
|
||||
/// Kernel flags: kernel image is compressed per the `compression` field.
|
||||
pub const KERNEL_FLAG_COMPRESSED: u32 = 1 << 10;
|
||||
/// Kernel flags: kernel must run inside a TEE enclave.
|
||||
pub const KERNEL_FLAG_REQUIRES_TEE: u32 = 1 << 0;
|
||||
/// Kernel flags: kernel measurement stored in WITNESS_SEG.
|
||||
pub const KERNEL_FLAG_MEASURED: u32 = 1 << 9;
|
||||
/// Kernel flags: kernel requires KVM (hardware virtualization).
|
||||
pub const KERNEL_FLAG_REQUIRES_KVM: u32 = 1 << 1;
|
||||
/// Kernel flags: kernel requires UEFI boot.
|
||||
pub const KERNEL_FLAG_REQUIRES_UEFI: u32 = 1 << 2;
|
||||
/// Kernel flags: kernel includes network stack.
|
||||
pub const KERNEL_FLAG_HAS_NETWORKING: u32 = 1 << 3;
|
||||
/// Kernel flags: kernel exposes RVF query API on api_port.
|
||||
pub const KERNEL_FLAG_HAS_QUERY_API: u32 = 1 << 4;
|
||||
/// Kernel flags: kernel exposes RVF ingest API.
|
||||
pub const KERNEL_FLAG_HAS_INGEST_API: u32 = 1 << 5;
|
||||
/// Kernel flags: kernel exposes health/metrics API.
|
||||
pub const KERNEL_FLAG_HAS_ADMIN_API: u32 = 1 << 6;
|
||||
/// Kernel flags: kernel can generate TEE attestation quotes.
|
||||
pub const KERNEL_FLAG_ATTESTATION_READY: u32 = 1 << 7;
|
||||
/// Kernel flags: kernel is position-independent.
|
||||
pub const KERNEL_FLAG_RELOCATABLE: u32 = 1 << 11;
|
||||
/// Kernel flags: kernel includes VirtIO network driver.
|
||||
pub const KERNEL_FLAG_HAS_VIRTIO_NET: u32 = 1 << 12;
|
||||
/// Kernel flags: kernel includes VirtIO block driver.
|
||||
pub const KERNEL_FLAG_HAS_VIRTIO_BLK: u32 = 1 << 13;
|
||||
/// Kernel flags: kernel includes VSOCK for host communication.
|
||||
pub const KERNEL_FLAG_HAS_VSOCK: u32 = 1 << 14;
|
||||
|
||||
/// Target CPU architecture for the kernel image.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum KernelArch {
|
||||
/// AMD64 / Intel 64.
|
||||
X86_64 = 0x00,
|
||||
/// ARM 64-bit (ARMv8-A and later).
|
||||
Aarch64 = 0x01,
|
||||
/// RISC-V 64-bit (RV64GC).
|
||||
Riscv64 = 0x02,
|
||||
/// Architecture-independent (e.g., interpreted).
|
||||
Universal = 0xFE,
|
||||
/// Reserved / unspecified.
|
||||
Unknown = 0xFF,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for KernelArch {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::X86_64),
|
||||
0x01 => Ok(Self::Aarch64),
|
||||
0x02 => Ok(Self::Riscv64),
|
||||
0xFE => Ok(Self::Universal),
|
||||
0xFF => Ok(Self::Unknown),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "KernelArch",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kernel type / runtime model.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum KernelType {
|
||||
/// Hermit OS unikernel (Rust-native).
|
||||
Hermit = 0x00,
|
||||
/// Minimal Linux kernel (bzImage compatible).
|
||||
MicroLinux = 0x01,
|
||||
/// Asterinas framekernel (Linux ABI compatible).
|
||||
Asterinas = 0x02,
|
||||
/// WASI Preview 2 component (alternative to WASM_SEG).
|
||||
WasiPreview2 = 0x03,
|
||||
/// Custom kernel (requires external VMM knowledge).
|
||||
Custom = 0x04,
|
||||
/// Test stub for CI (boots, reports health, exits).
|
||||
TestStub = 0xFE,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for KernelType {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::Hermit),
|
||||
0x01 => Ok(Self::MicroLinux),
|
||||
0x02 => Ok(Self::Asterinas),
|
||||
0x03 => Ok(Self::WasiPreview2),
|
||||
0x04 => Ok(Self::Custom),
|
||||
0xFE => Ok(Self::TestStub),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "KernelType",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transport mechanism for the kernel's query API.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum ApiTransport {
|
||||
/// HTTP/1.1 over TCP (default).
|
||||
TcpHttp = 0x00,
|
||||
/// gRPC over TCP (HTTP/2).
|
||||
TcpGrpc = 0x01,
|
||||
/// VirtIO socket (Firecracker host<->guest).
|
||||
Vsock = 0x02,
|
||||
/// Shared memory region (for same-host co-location).
|
||||
SharedMem = 0x03,
|
||||
/// No network API (batch mode only).
|
||||
None = 0xFF,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ApiTransport {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::TcpHttp),
|
||||
0x01 => Ok(Self::TcpGrpc),
|
||||
0x02 => Ok(Self::Vsock),
|
||||
0x03 => Ok(Self::SharedMem),
|
||||
0xFF => Ok(Self::None),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "ApiTransport",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 128-byte header for KERNEL_SEG payloads.
|
||||
///
|
||||
/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
|
||||
/// little-endian on the wire except `api_port` which is network byte order
|
||||
/// (big-endian) per ADR-030.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct KernelHeader {
|
||||
/// Magic: `KERNEL_MAGIC` (0x52564B4E, "RVKN").
|
||||
pub kernel_magic: u32,
|
||||
/// KernelHeader format version (currently 1).
|
||||
pub header_version: u16,
|
||||
/// Target architecture (see `KernelArch`).
|
||||
pub arch: u8,
|
||||
/// Kernel type (see `KernelType`).
|
||||
pub kernel_type: u8,
|
||||
/// Bitfield flags (see `KERNEL_FLAG_*` constants).
|
||||
pub kernel_flags: u32,
|
||||
/// Minimum RAM required (MiB).
|
||||
pub min_memory_mb: u32,
|
||||
/// Virtual address of kernel entry point.
|
||||
pub entry_point: u64,
|
||||
/// Uncompressed kernel image size (bytes).
|
||||
pub image_size: u64,
|
||||
/// Compressed kernel image size (bytes).
|
||||
pub compressed_size: u64,
|
||||
/// Compression algorithm (same enum as `SegmentHeader.compression`).
|
||||
pub compression: u8,
|
||||
/// API transport (see `ApiTransport`).
|
||||
pub api_transport: u8,
|
||||
/// Default API port (network byte order).
|
||||
pub api_port: u16,
|
||||
/// Supported RVF query API version.
|
||||
pub api_version: u32,
|
||||
/// SHAKE-256-256 of uncompressed kernel image.
|
||||
pub image_hash: [u8; 32],
|
||||
/// Unique build identifier (UUID v7).
|
||||
pub build_id: [u8; 16],
|
||||
/// Build time (nanosecond UNIX timestamp).
|
||||
pub build_timestamp: u64,
|
||||
/// Recommended vCPU count (0 = single).
|
||||
pub vcpu_count: u32,
|
||||
/// Reserved (must be zero).
|
||||
pub reserved_0: u32,
|
||||
/// Offset to kernel command line within payload.
|
||||
pub cmdline_offset: u64,
|
||||
/// Length of kernel command line (bytes).
|
||||
pub cmdline_length: u32,
|
||||
/// Reserved (must be zero).
|
||||
pub reserved_1: u32,
|
||||
}
|
||||
|
||||
// Compile-time assertion: KernelHeader must be exactly 128 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<KernelHeader>() == 128);
|
||||
|
||||
impl KernelHeader {
|
||||
/// Serialize the header to a 128-byte little-endian array.
|
||||
pub fn to_bytes(&self) -> [u8; 128] {
|
||||
let mut buf = [0u8; 128];
|
||||
buf[0x00..0x04].copy_from_slice(&self.kernel_magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.header_version.to_le_bytes());
|
||||
buf[0x06] = self.arch;
|
||||
buf[0x07] = self.kernel_type;
|
||||
buf[0x08..0x0C].copy_from_slice(&self.kernel_flags.to_le_bytes());
|
||||
buf[0x0C..0x10].copy_from_slice(&self.min_memory_mb.to_le_bytes());
|
||||
buf[0x10..0x18].copy_from_slice(&self.entry_point.to_le_bytes());
|
||||
buf[0x18..0x20].copy_from_slice(&self.image_size.to_le_bytes());
|
||||
buf[0x20..0x28].copy_from_slice(&self.compressed_size.to_le_bytes());
|
||||
buf[0x28] = self.compression;
|
||||
buf[0x29] = self.api_transport;
|
||||
buf[0x2A..0x2C].copy_from_slice(&self.api_port.to_be_bytes());
|
||||
buf[0x2C..0x30].copy_from_slice(&self.api_version.to_le_bytes());
|
||||
buf[0x30..0x50].copy_from_slice(&self.image_hash);
|
||||
buf[0x50..0x60].copy_from_slice(&self.build_id);
|
||||
buf[0x60..0x68].copy_from_slice(&self.build_timestamp.to_le_bytes());
|
||||
buf[0x68..0x6C].copy_from_slice(&self.vcpu_count.to_le_bytes());
|
||||
buf[0x6C..0x70].copy_from_slice(&self.reserved_0.to_le_bytes());
|
||||
buf[0x70..0x78].copy_from_slice(&self.cmdline_offset.to_le_bytes());
|
||||
buf[0x78..0x7C].copy_from_slice(&self.cmdline_length.to_le_bytes());
|
||||
buf[0x7C..0x80].copy_from_slice(&self.reserved_1.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize a `KernelHeader` from a 128-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 128]) -> Result<Self, RvfError> {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != KERNEL_MAGIC {
|
||||
return Err(RvfError::BadMagic {
|
||||
expected: KERNEL_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
kernel_magic: magic,
|
||||
header_version: u16::from_le_bytes([data[0x04], data[0x05]]),
|
||||
arch: data[0x06],
|
||||
kernel_type: data[0x07],
|
||||
kernel_flags: u32::from_le_bytes([data[0x08], data[0x09], data[0x0A], data[0x0B]]),
|
||||
min_memory_mb: u32::from_le_bytes([data[0x0C], data[0x0D], data[0x0E], data[0x0F]]),
|
||||
entry_point: u64::from_le_bytes([
|
||||
data[0x10], data[0x11], data[0x12], data[0x13], data[0x14], data[0x15], data[0x16],
|
||||
data[0x17],
|
||||
]),
|
||||
image_size: u64::from_le_bytes([
|
||||
data[0x18], data[0x19], data[0x1A], data[0x1B], data[0x1C], data[0x1D], data[0x1E],
|
||||
data[0x1F],
|
||||
]),
|
||||
compressed_size: u64::from_le_bytes([
|
||||
data[0x20], data[0x21], data[0x22], data[0x23], data[0x24], data[0x25], data[0x26],
|
||||
data[0x27],
|
||||
]),
|
||||
compression: data[0x28],
|
||||
api_transport: data[0x29],
|
||||
api_port: u16::from_be_bytes([data[0x2A], data[0x2B]]),
|
||||
api_version: u32::from_le_bytes([data[0x2C], data[0x2D], data[0x2E], data[0x2F]]),
|
||||
image_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x30..0x50]);
|
||||
h
|
||||
},
|
||||
build_id: {
|
||||
let mut id = [0u8; 16];
|
||||
id.copy_from_slice(&data[0x50..0x60]);
|
||||
id
|
||||
},
|
||||
build_timestamp: u64::from_le_bytes([
|
||||
data[0x60], data[0x61], data[0x62], data[0x63], data[0x64], data[0x65], data[0x66],
|
||||
data[0x67],
|
||||
]),
|
||||
vcpu_count: u32::from_le_bytes([data[0x68], data[0x69], data[0x6A], data[0x6B]]),
|
||||
reserved_0: u32::from_le_bytes([data[0x6C], data[0x6D], data[0x6E], data[0x6F]]),
|
||||
cmdline_offset: u64::from_le_bytes([
|
||||
data[0x70], data[0x71], data[0x72], data[0x73], data[0x74], data[0x75], data[0x76],
|
||||
data[0x77],
|
||||
]),
|
||||
cmdline_length: u32::from_le_bytes([data[0x78], data[0x79], data[0x7A], data[0x7B]]),
|
||||
reserved_1: u32::from_le_bytes([data[0x7C], data[0x7D], data[0x7E], data[0x7F]]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_header() -> KernelHeader {
|
||||
KernelHeader {
|
||||
kernel_magic: KERNEL_MAGIC,
|
||||
header_version: 1,
|
||||
arch: KernelArch::X86_64 as u8,
|
||||
kernel_type: KernelType::Hermit as u8,
|
||||
kernel_flags: KERNEL_FLAG_HAS_QUERY_API | KERNEL_FLAG_COMPRESSED,
|
||||
min_memory_mb: 32,
|
||||
entry_point: 0x0020_0000,
|
||||
image_size: 400_000,
|
||||
compressed_size: 180_000,
|
||||
compression: 2, // ZSTD
|
||||
api_transport: ApiTransport::TcpHttp as u8,
|
||||
api_port: 8080,
|
||||
api_version: 1,
|
||||
image_hash: [0xAB; 32],
|
||||
build_id: [0xCD; 16],
|
||||
build_timestamp: 1_700_000_000_000_000_000,
|
||||
vcpu_count: 1,
|
||||
reserved_0: 0,
|
||||
cmdline_offset: 128,
|
||||
cmdline_length: 64,
|
||||
reserved_1: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_128() {
|
||||
assert_eq!(core::mem::size_of::<KernelHeader>(), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
let bytes_be = KERNEL_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVKN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_header();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = KernelHeader::from_bytes(&bytes).expect("from_bytes should succeed");
|
||||
|
||||
assert_eq!(decoded.kernel_magic, KERNEL_MAGIC);
|
||||
assert_eq!(decoded.header_version, 1);
|
||||
assert_eq!(decoded.arch, KernelArch::X86_64 as u8);
|
||||
assert_eq!(decoded.kernel_type, KernelType::Hermit as u8);
|
||||
assert_eq!(
|
||||
decoded.kernel_flags,
|
||||
KERNEL_FLAG_HAS_QUERY_API | KERNEL_FLAG_COMPRESSED
|
||||
);
|
||||
assert_eq!(decoded.min_memory_mb, 32);
|
||||
assert_eq!(decoded.entry_point, 0x0020_0000);
|
||||
assert_eq!(decoded.image_size, 400_000);
|
||||
assert_eq!(decoded.compressed_size, 180_000);
|
||||
assert_eq!(decoded.compression, 2);
|
||||
assert_eq!(decoded.api_transport, ApiTransport::TcpHttp as u8);
|
||||
assert_eq!(decoded.api_port, 8080);
|
||||
assert_eq!(decoded.api_version, 1);
|
||||
assert_eq!(decoded.image_hash, [0xAB; 32]);
|
||||
assert_eq!(decoded.build_id, [0xCD; 16]);
|
||||
assert_eq!(decoded.build_timestamp, 1_700_000_000_000_000_000);
|
||||
assert_eq!(decoded.vcpu_count, 1);
|
||||
assert_eq!(decoded.reserved_0, 0);
|
||||
assert_eq!(decoded.cmdline_offset, 128);
|
||||
assert_eq!(decoded.cmdline_length, 64);
|
||||
assert_eq!(decoded.reserved_1, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_magic_returns_error() {
|
||||
let mut bytes = sample_header().to_bytes();
|
||||
bytes[0] = 0x00; // corrupt magic
|
||||
let err = KernelHeader::from_bytes(&bytes).unwrap_err();
|
||||
match err {
|
||||
RvfError::BadMagic { expected, .. } => assert_eq!(expected, KERNEL_MAGIC),
|
||||
other => panic!("expected BadMagic, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let h = sample_header();
|
||||
let base = &h as *const _ as usize;
|
||||
|
||||
assert_eq!(&h.kernel_magic as *const _ as usize - base, 0x00);
|
||||
assert_eq!(&h.header_version as *const _ as usize - base, 0x04);
|
||||
assert_eq!(&h.arch as *const _ as usize - base, 0x06);
|
||||
assert_eq!(&h.kernel_type as *const _ as usize - base, 0x07);
|
||||
assert_eq!(&h.kernel_flags as *const _ as usize - base, 0x08);
|
||||
assert_eq!(&h.min_memory_mb as *const _ as usize - base, 0x0C);
|
||||
assert_eq!(&h.entry_point as *const _ as usize - base, 0x10);
|
||||
assert_eq!(&h.image_size as *const _ as usize - base, 0x18);
|
||||
assert_eq!(&h.compressed_size as *const _ as usize - base, 0x20);
|
||||
assert_eq!(&h.compression as *const _ as usize - base, 0x28);
|
||||
assert_eq!(&h.api_transport as *const _ as usize - base, 0x29);
|
||||
assert_eq!(&h.api_port as *const _ as usize - base, 0x2A);
|
||||
assert_eq!(&h.api_version as *const _ as usize - base, 0x2C);
|
||||
assert_eq!(&h.image_hash as *const _ as usize - base, 0x30);
|
||||
assert_eq!(&h.build_id as *const _ as usize - base, 0x50);
|
||||
assert_eq!(&h.build_timestamp as *const _ as usize - base, 0x60);
|
||||
assert_eq!(&h.vcpu_count as *const _ as usize - base, 0x68);
|
||||
assert_eq!(&h.reserved_0 as *const _ as usize - base, 0x6C);
|
||||
assert_eq!(&h.cmdline_offset as *const _ as usize - base, 0x70);
|
||||
assert_eq!(&h.cmdline_length as *const _ as usize - base, 0x78);
|
||||
assert_eq!(&h.reserved_1 as *const _ as usize - base, 0x7C);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_arch_try_from() {
|
||||
assert_eq!(KernelArch::try_from(0x00), Ok(KernelArch::X86_64));
|
||||
assert_eq!(KernelArch::try_from(0x01), Ok(KernelArch::Aarch64));
|
||||
assert_eq!(KernelArch::try_from(0x02), Ok(KernelArch::Riscv64));
|
||||
assert_eq!(KernelArch::try_from(0xFE), Ok(KernelArch::Universal));
|
||||
assert_eq!(KernelArch::try_from(0xFF), Ok(KernelArch::Unknown));
|
||||
assert!(KernelArch::try_from(0x03).is_err());
|
||||
assert!(KernelArch::try_from(0x80).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_type_try_from() {
|
||||
assert_eq!(KernelType::try_from(0x00), Ok(KernelType::Hermit));
|
||||
assert_eq!(KernelType::try_from(0x01), Ok(KernelType::MicroLinux));
|
||||
assert_eq!(KernelType::try_from(0x02), Ok(KernelType::Asterinas));
|
||||
assert_eq!(KernelType::try_from(0x03), Ok(KernelType::WasiPreview2));
|
||||
assert_eq!(KernelType::try_from(0x04), Ok(KernelType::Custom));
|
||||
assert_eq!(KernelType::try_from(0xFE), Ok(KernelType::TestStub));
|
||||
assert!(KernelType::try_from(0x05).is_err());
|
||||
assert!(KernelType::try_from(0xFF).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_transport_try_from() {
|
||||
assert_eq!(ApiTransport::try_from(0x00), Ok(ApiTransport::TcpHttp));
|
||||
assert_eq!(ApiTransport::try_from(0x01), Ok(ApiTransport::TcpGrpc));
|
||||
assert_eq!(ApiTransport::try_from(0x02), Ok(ApiTransport::Vsock));
|
||||
assert_eq!(ApiTransport::try_from(0x03), Ok(ApiTransport::SharedMem));
|
||||
assert_eq!(ApiTransport::try_from(0xFF), Ok(ApiTransport::None));
|
||||
assert!(ApiTransport::try_from(0x04).is_err());
|
||||
assert!(ApiTransport::try_from(0x80).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_flags_bit_positions() {
|
||||
assert_eq!(KERNEL_FLAG_REQUIRES_TEE, 0x0001);
|
||||
assert_eq!(KERNEL_FLAG_REQUIRES_KVM, 0x0002);
|
||||
assert_eq!(KERNEL_FLAG_REQUIRES_UEFI, 0x0004);
|
||||
assert_eq!(KERNEL_FLAG_HAS_NETWORKING, 0x0008);
|
||||
assert_eq!(KERNEL_FLAG_HAS_QUERY_API, 0x0010);
|
||||
assert_eq!(KERNEL_FLAG_HAS_INGEST_API, 0x0020);
|
||||
assert_eq!(KERNEL_FLAG_HAS_ADMIN_API, 0x0040);
|
||||
assert_eq!(KERNEL_FLAG_ATTESTATION_READY, 0x0080);
|
||||
assert_eq!(KERNEL_FLAG_SIGNED, 0x0100);
|
||||
assert_eq!(KERNEL_FLAG_MEASURED, 0x0200);
|
||||
assert_eq!(KERNEL_FLAG_COMPRESSED, 0x0400);
|
||||
assert_eq!(KERNEL_FLAG_RELOCATABLE, 0x0800);
|
||||
assert_eq!(KERNEL_FLAG_HAS_VIRTIO_NET, 0x1000);
|
||||
assert_eq!(KERNEL_FLAG_HAS_VIRTIO_BLK, 0x2000);
|
||||
assert_eq!(KERNEL_FLAG_HAS_VSOCK, 0x4000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_port_network_byte_order() {
|
||||
let mut h = sample_header();
|
||||
h.api_port = 0x1F90; // 8080
|
||||
let bytes = h.to_bytes();
|
||||
// api_port at offset 0x2A, big-endian
|
||||
assert_eq!(bytes[0x2A], 0x1F);
|
||||
assert_eq!(bytes[0x2B], 0x90);
|
||||
let decoded = KernelHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.api_port, 0x1F90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_filled_reserved_fields() {
|
||||
let h = sample_header();
|
||||
let bytes = h.to_bytes();
|
||||
// reserved_0 at 0x6C..0x70 should be zero
|
||||
assert_eq!(&bytes[0x6C..0x70], &[0, 0, 0, 0]);
|
||||
// reserved_1 at 0x7C..0x80 should be zero
|
||||
assert_eq!(&bytes[0x7C..0x80], &[0, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
186
crates/rvf/rvf-types/src/kernel_binding.rs
Normal file
186
crates/rvf/rvf-types/src/kernel_binding.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
//! Kernel binding types for the RVF computational container.
|
||||
//!
|
||||
//! Defines the 128-byte `KernelBinding` struct per ADR-031 (revised).
|
||||
//! A `KernelBinding` cryptographically ties a manifest root to a
|
||||
//! policy hash with a version stamp, ensuring tamper-evident linkage.
|
||||
//!
|
||||
//! Padded to 128 bytes to avoid future wire-format breaks. Active fields
|
||||
//! occupy 76 bytes; the remaining 52 bytes are reserved/padding (must be zero).
|
||||
|
||||
/// 128-byte kernel binding record (padded for future evolution).
|
||||
///
|
||||
/// Layout:
|
||||
/// | Offset | Size | Field |
|
||||
/// |--------|------|----------------------|
|
||||
/// | 0x00 | 32 | manifest_root_hash |
|
||||
/// | 0x20 | 32 | policy_hash |
|
||||
/// | 0x40 | 2 | binding_version |
|
||||
/// | 0x42 | 2 | min_runtime_version |
|
||||
/// | 0x44 | 4 | _pad0 (alignment) |
|
||||
/// | 0x48 | 8 | allowed_segment_mask |
|
||||
/// | 0x50 | 48 | _reserved |
|
||||
///
|
||||
/// All multi-byte fields are little-endian on the wire.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct KernelBinding {
|
||||
/// SHAKE-256-256 of the manifest root node.
|
||||
pub manifest_root_hash: [u8; 32],
|
||||
/// SHAKE-256-256 of the policy document.
|
||||
pub policy_hash: [u8; 32],
|
||||
/// Binding format version (currently 1).
|
||||
pub binding_version: u16,
|
||||
/// Minimum runtime version required (0 = any).
|
||||
pub min_runtime_version: u16,
|
||||
/// Alignment padding (must be zero).
|
||||
pub _pad0: u32,
|
||||
/// Bitmask of allowed segment types (0 = no restriction).
|
||||
pub allowed_segment_mask: u64,
|
||||
/// Reserved for future use (must be zero).
|
||||
pub _reserved: [u8; 48],
|
||||
}
|
||||
|
||||
// Compile-time assertion: KernelBinding must be exactly 128 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<KernelBinding>() == 128);
|
||||
|
||||
impl KernelBinding {
|
||||
/// Serialize the binding to a 128-byte array.
|
||||
pub fn to_bytes(&self) -> [u8; 128] {
|
||||
let mut buf = [0u8; 128];
|
||||
buf[0x00..0x20].copy_from_slice(&self.manifest_root_hash);
|
||||
buf[0x20..0x40].copy_from_slice(&self.policy_hash);
|
||||
buf[0x40..0x42].copy_from_slice(&self.binding_version.to_le_bytes());
|
||||
buf[0x42..0x44].copy_from_slice(&self.min_runtime_version.to_le_bytes());
|
||||
buf[0x44..0x48].copy_from_slice(&self._pad0.to_le_bytes());
|
||||
buf[0x48..0x50].copy_from_slice(&self.allowed_segment_mask.to_le_bytes());
|
||||
buf[0x50..0x80].copy_from_slice(&self._reserved);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize a `KernelBinding` from a 128-byte slice (unchecked).
|
||||
///
|
||||
/// Does NOT validate reserved fields. Use `from_bytes_validated` for
|
||||
/// security-critical paths that must reject non-zero padding/reserved.
|
||||
pub fn from_bytes(data: &[u8; 128]) -> Self {
|
||||
Self {
|
||||
manifest_root_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x00..0x20]);
|
||||
h
|
||||
},
|
||||
policy_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x20..0x40]);
|
||||
h
|
||||
},
|
||||
binding_version: u16::from_le_bytes([data[0x40], data[0x41]]),
|
||||
min_runtime_version: u16::from_le_bytes([data[0x42], data[0x43]]),
|
||||
_pad0: u32::from_le_bytes([data[0x44], data[0x45], data[0x46], data[0x47]]),
|
||||
allowed_segment_mask: u64::from_le_bytes([
|
||||
data[0x48], data[0x49], data[0x4A], data[0x4B], data[0x4C], data[0x4D], data[0x4E],
|
||||
data[0x4F],
|
||||
]),
|
||||
_reserved: {
|
||||
let mut r = [0u8; 48];
|
||||
r.copy_from_slice(&data[0x50..0x80]);
|
||||
r
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize and validate a `KernelBinding` from a 128-byte slice.
|
||||
///
|
||||
/// Rejects bindings where:
|
||||
/// - `binding_version` is 0 (uninitialized)
|
||||
/// - `_pad0` is non-zero (spec violation)
|
||||
/// - `_reserved` contains non-zero bytes (spec violation / data smuggling)
|
||||
pub fn from_bytes_validated(data: &[u8; 128]) -> Result<Self, &'static str> {
|
||||
let binding = Self::from_bytes(data);
|
||||
if binding.binding_version == 0 {
|
||||
return Err("binding_version must be > 0");
|
||||
}
|
||||
if binding._pad0 != 0 {
|
||||
return Err("_pad0 must be zero");
|
||||
}
|
||||
if binding._reserved.iter().any(|&b| b != 0) {
|
||||
return Err("_reserved must be all zeros");
|
||||
}
|
||||
Ok(binding)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_binding() -> KernelBinding {
|
||||
KernelBinding {
|
||||
manifest_root_hash: [0xAA; 32],
|
||||
policy_hash: [0xBB; 32],
|
||||
binding_version: 1,
|
||||
min_runtime_version: 0,
|
||||
_pad0: 0,
|
||||
allowed_segment_mask: 0,
|
||||
_reserved: [0; 48],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binding_size_is_128() {
|
||||
assert_eq!(core::mem::size_of::<KernelBinding>(), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_binding();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = KernelBinding::from_bytes(&bytes);
|
||||
|
||||
assert_eq!(decoded.manifest_root_hash, [0xAA; 32]);
|
||||
assert_eq!(decoded.policy_hash, [0xBB; 32]);
|
||||
assert_eq!(decoded.binding_version, 1);
|
||||
assert_eq!(decoded.min_runtime_version, 0);
|
||||
assert_eq!(decoded._pad0, 0);
|
||||
assert_eq!(decoded.allowed_segment_mask, 0);
|
||||
assert_eq!(decoded._reserved, [0; 48]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_with_fields() {
|
||||
let binding = KernelBinding {
|
||||
manifest_root_hash: [0x11; 32],
|
||||
policy_hash: [0x22; 32],
|
||||
binding_version: 2,
|
||||
min_runtime_version: 3,
|
||||
_pad0: 0,
|
||||
allowed_segment_mask: 0x00FF_FFFF,
|
||||
_reserved: [0; 48],
|
||||
};
|
||||
let bytes = binding.to_bytes();
|
||||
let decoded = KernelBinding::from_bytes(&bytes);
|
||||
assert_eq!(decoded.binding_version, 2);
|
||||
assert_eq!(decoded.min_runtime_version, 3);
|
||||
assert_eq!(decoded.allowed_segment_mask, 0x00FF_FFFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let b = sample_binding();
|
||||
let base = &b as *const _ as usize;
|
||||
|
||||
assert_eq!(&b.manifest_root_hash as *const _ as usize - base, 0x00);
|
||||
assert_eq!(&b.policy_hash as *const _ as usize - base, 0x20);
|
||||
assert_eq!(&b.binding_version as *const _ as usize - base, 0x40);
|
||||
assert_eq!(&b.min_runtime_version as *const _ as usize - base, 0x42);
|
||||
assert_eq!(&b._pad0 as *const _ as usize - base, 0x44);
|
||||
assert_eq!(&b.allowed_segment_mask as *const _ as usize - base, 0x48);
|
||||
assert_eq!(&b._reserved as *const _ as usize - base, 0x50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_must_be_zero_in_new_bindings() {
|
||||
let b = sample_binding();
|
||||
assert!(b._reserved.iter().all(|&x| x == 0));
|
||||
assert_eq!(b._pad0, 0);
|
||||
}
|
||||
}
|
||||
125
crates/rvf/rvf-types/src/lib.rs
Normal file
125
crates/rvf/rvf-types/src/lib.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
//! Core types for the RuVector Format (RVF).
|
||||
//!
|
||||
//! This crate provides the foundational types shared across all RVF crates:
|
||||
//! segment headers, type enums, flags, error codes, and format constants.
|
||||
//!
|
||||
//! All types are `no_std` compatible by default.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
extern crate alloc;
|
||||
|
||||
// Tests always need alloc (for Vec, format!, etc.) even without the feature.
|
||||
#[cfg(all(test, not(feature = "alloc")))]
|
||||
extern crate alloc;
|
||||
|
||||
pub mod agi_container;
|
||||
pub mod attestation;
|
||||
pub mod checksum;
|
||||
pub mod compression;
|
||||
pub mod constants;
|
||||
pub mod cow_map;
|
||||
pub mod dashboard;
|
||||
pub mod data_type;
|
||||
pub mod delta;
|
||||
pub mod ebpf;
|
||||
#[cfg(feature = "ed25519")]
|
||||
pub mod ed25519;
|
||||
pub mod error;
|
||||
pub mod filter;
|
||||
pub mod flags;
|
||||
pub mod kernel;
|
||||
pub mod kernel_binding;
|
||||
pub mod lineage;
|
||||
pub mod manifest;
|
||||
pub mod membership;
|
||||
pub mod profile;
|
||||
pub mod qr_seed;
|
||||
pub mod quality;
|
||||
pub mod quant_type;
|
||||
pub mod refcount;
|
||||
pub mod security;
|
||||
pub mod segment;
|
||||
pub mod segment_type;
|
||||
pub mod sha256;
|
||||
pub mod signature;
|
||||
pub mod wasm_bootstrap;
|
||||
pub mod witness;
|
||||
|
||||
pub use agi_container::{
|
||||
AgiContainerHeader, AuthorityLevel, CoherenceThresholds, ContainerError, ContainerSegments,
|
||||
ExecutionMode, ResourceBudget, AGI_HAS_COHERENCE_GATES, AGI_HAS_DOMAIN_EXPANSION, AGI_HAS_EVAL,
|
||||
AGI_HAS_KERNEL, AGI_HAS_ORCHESTRATOR, AGI_HAS_SKILLS, AGI_HAS_TOOLS, AGI_HAS_WASM,
|
||||
AGI_HAS_WITNESS, AGI_HAS_WORLD_MODEL, AGI_HEADER_SIZE, AGI_MAGIC, AGI_MAX_CONTAINER_SIZE,
|
||||
AGI_OFFLINE_CAPABLE, AGI_REPLAY_CAPABLE, AGI_SIGNED, AGI_TAG_AUTHORITY_CONFIG,
|
||||
AGI_TAG_COST_CURVE, AGI_TAG_COUNTEREXAMPLES, AGI_TAG_DOMAIN_PROFILE, AGI_TAG_POLICY_KERNEL,
|
||||
AGI_TAG_TRANSFER_PRIOR,
|
||||
};
|
||||
pub use attestation::{AttestationHeader, AttestationWitnessType, TeePlatform, KEY_TYPE_TEE_BOUND};
|
||||
pub use checksum::ChecksumAlgo;
|
||||
pub use compression::CompressionAlgo;
|
||||
pub use constants::*;
|
||||
pub use cow_map::{CowMapEntry, CowMapHeader, MapFormat, COWMAP_MAGIC};
|
||||
pub use dashboard::{DashboardHeader, DASHBOARD_MAGIC, DASHBOARD_MAX_SIZE};
|
||||
pub use data_type::DataType;
|
||||
pub use delta::{DeltaEncoding, DeltaHeader, DELTA_MAGIC};
|
||||
pub use ebpf::{EbpfAttachType, EbpfHeader, EbpfProgramType, EBPF_MAGIC};
|
||||
#[cfg(feature = "ed25519")]
|
||||
pub use ed25519::{
|
||||
ct_eq_sig, ed25519_sign, ed25519_verify, Ed25519Keypair,
|
||||
PUBLIC_KEY_SIZE as ED25519_PUBLIC_KEY_SIZE, SECRET_KEY_SIZE as ED25519_SECRET_KEY_SIZE,
|
||||
SIGNATURE_SIZE as ED25519_SIGNATURE_SIZE,
|
||||
};
|
||||
pub use error::{ErrorCode, RvfError};
|
||||
pub use filter::FilterOp;
|
||||
pub use flags::SegmentFlags;
|
||||
pub use kernel::{
|
||||
ApiTransport, KernelArch, KernelHeader, KernelType, KERNEL_FLAG_ATTESTATION_READY,
|
||||
KERNEL_FLAG_COMPRESSED, KERNEL_FLAG_HAS_ADMIN_API, KERNEL_FLAG_HAS_INGEST_API,
|
||||
KERNEL_FLAG_HAS_NETWORKING, KERNEL_FLAG_HAS_QUERY_API, KERNEL_FLAG_HAS_VIRTIO_BLK,
|
||||
KERNEL_FLAG_HAS_VIRTIO_NET, KERNEL_FLAG_HAS_VSOCK, KERNEL_FLAG_MEASURED,
|
||||
KERNEL_FLAG_RELOCATABLE, KERNEL_FLAG_REQUIRES_KVM, KERNEL_FLAG_REQUIRES_TEE,
|
||||
KERNEL_FLAG_REQUIRES_UEFI, KERNEL_FLAG_SIGNED, KERNEL_MAGIC,
|
||||
};
|
||||
pub use kernel_binding::KernelBinding;
|
||||
pub use lineage::{
|
||||
DerivationType, FileIdentity, LineageRecord, LINEAGE_RECORD_SIZE, WITNESS_DERIVATION,
|
||||
WITNESS_LINEAGE_MERGE, WITNESS_LINEAGE_SNAPSHOT, WITNESS_LINEAGE_TRANSFORM,
|
||||
WITNESS_LINEAGE_VERIFY,
|
||||
};
|
||||
pub use manifest::{
|
||||
CentroidPtr, EntrypointPtr, HotCachePtr, Level0Root, PrefetchMapPtr, QuantDictPtr, TopLayerPtr,
|
||||
};
|
||||
pub use membership::{FilterMode, FilterType, MembershipHeader, MEMBERSHIP_MAGIC};
|
||||
pub use profile::{DomainProfile, ProfileId};
|
||||
pub use qr_seed::{
|
||||
HostEntry, LayerEntry, SeedHeader, QR_MAX_BYTES, SEED_COMPRESSED, SEED_ENCRYPTED,
|
||||
SEED_HAS_DOWNLOAD, SEED_HAS_MICROKERNEL, SEED_HAS_VECTORS, SEED_HEADER_SIZE, SEED_MAGIC,
|
||||
SEED_OFFLINE_CAPABLE, SEED_SIGNED, SEED_STREAM_UPGRADE,
|
||||
};
|
||||
pub use quality::{
|
||||
derive_response_quality, BudgetReport, BudgetType, DegradationReason, DegradationReport,
|
||||
FallbackPath, IndexLayersUsed, QualityPreference, ResponseQuality, RetrievalQuality,
|
||||
SafetyNetBudget, SearchEvidenceSummary,
|
||||
};
|
||||
pub use quant_type::QuantType;
|
||||
pub use refcount::{RefcountHeader, REFCOUNT_MAGIC};
|
||||
pub use security::{HardeningFields, SecurityError, SecurityPolicy};
|
||||
pub use segment::SegmentHeader;
|
||||
pub use segment_type::SegmentType;
|
||||
pub use sha256::{hmac_sha256, sha256, Sha256};
|
||||
pub use signature::{SignatureAlgo, SignatureFooter};
|
||||
pub use wasm_bootstrap::{
|
||||
WasmHeader, WasmRole, WasmTarget, WASM_FEAT_BULK_MEMORY, WASM_FEAT_EXCEPTION_HANDLING,
|
||||
WASM_FEAT_GC, WASM_FEAT_MULTI_VALUE, WASM_FEAT_REFERENCE_TYPES, WASM_FEAT_SIMD,
|
||||
WASM_FEAT_TAIL_CALL, WASM_FEAT_THREADS, WASM_MAGIC,
|
||||
};
|
||||
pub use witness::{
|
||||
GovernanceMode, PolicyCheck, Scorecard, TaskOutcome, WitnessHeader, WITNESS_HEADER_SIZE,
|
||||
WITNESS_MAGIC, WIT_HAS_DIFF, WIT_HAS_PLAN, WIT_HAS_POSTMORTEM, WIT_HAS_SPEC, WIT_HAS_TEST_LOG,
|
||||
WIT_HAS_TRACE, WIT_SIGNED, WIT_TAG_DIFF, WIT_TAG_PLAN, WIT_TAG_POSTMORTEM, WIT_TAG_SPEC,
|
||||
WIT_TAG_TEST_LOG, WIT_TAG_TRACE,
|
||||
};
|
||||
#[cfg(feature = "alloc")]
|
||||
pub use witness::{ToolCallEntry, TOOL_CALL_FIXED_SIZE};
|
||||
337
crates/rvf/rvf-types/src/lineage.rs
Normal file
337
crates/rvf/rvf-types/src/lineage.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
//! DNA-style lineage provenance types for RVF files.
|
||||
//!
|
||||
//! Each RVF file carries a `FileIdentity` in the Level0Root reserved area,
|
||||
//! enabling provenance chains: parent→child→grandchild with hash verification.
|
||||
|
||||
/// Derivation type describing how a child file was produced from its parent.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum DerivationType {
|
||||
/// Exact copy of the parent.
|
||||
Clone = 0,
|
||||
/// Subset of parent data (filtered).
|
||||
Filter = 1,
|
||||
/// Multiple parents merged into one.
|
||||
Merge = 2,
|
||||
/// Re-quantized from parent.
|
||||
Quantize = 3,
|
||||
/// Re-indexed (HNSW rebuild, etc.).
|
||||
Reindex = 4,
|
||||
/// Arbitrary transformation.
|
||||
Transform = 5,
|
||||
/// Point-in-time snapshot.
|
||||
Snapshot = 6,
|
||||
/// User-defined derivation.
|
||||
UserDefined = 0xFF,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for DerivationType {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Clone),
|
||||
1 => Ok(Self::Filter),
|
||||
2 => Ok(Self::Merge),
|
||||
3 => Ok(Self::Quantize),
|
||||
4 => Ok(Self::Reindex),
|
||||
5 => Ok(Self::Transform),
|
||||
6 => Ok(Self::Snapshot),
|
||||
0xFF => Ok(Self::UserDefined),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// File identity embedded in the Level0Root reserved area at offset 0xF00.
|
||||
///
|
||||
/// Exactly 68 bytes, fitting within the 252-byte reserved area.
|
||||
/// Old readers that ignore the reserved area see zeros and continue working.
|
||||
///
|
||||
/// Layout:
|
||||
/// | Offset | Size | Field |
|
||||
/// |--------|------|----------------|
|
||||
/// | 0x00 | 16 | file_id |
|
||||
/// | 0x10 | 16 | parent_id |
|
||||
/// | 0x20 | 32 | parent_hash |
|
||||
/// | 0x40 | 4 | lineage_depth |
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct FileIdentity {
|
||||
/// Unique identifier for this file (UUID-style, 16 bytes).
|
||||
pub file_id: [u8; 16],
|
||||
/// Identifier of the parent file (all zeros for root files).
|
||||
pub parent_id: [u8; 16],
|
||||
/// SHAKE-256-256 hash of the parent's manifest (all zeros for root).
|
||||
pub parent_hash: [u8; 32],
|
||||
/// Lineage depth: 0 for root, incremented for each derivation.
|
||||
pub lineage_depth: u32,
|
||||
}
|
||||
|
||||
// Compile-time assertion: FileIdentity must be exactly 68 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<FileIdentity>() == 68);
|
||||
|
||||
impl FileIdentity {
|
||||
/// Create a root identity (no parent) with the given file_id.
|
||||
pub const fn new_root(file_id: [u8; 16]) -> Self {
|
||||
Self {
|
||||
file_id,
|
||||
parent_id: [0u8; 16],
|
||||
parent_hash: [0u8; 32],
|
||||
lineage_depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this is a root identity (no parent).
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.parent_id == [0u8; 16] && self.lineage_depth == 0
|
||||
}
|
||||
|
||||
/// Create an all-zero identity (default for files without lineage).
|
||||
pub const fn zeroed() -> Self {
|
||||
Self {
|
||||
file_id: [0u8; 16],
|
||||
parent_id: [0u8; 16],
|
||||
parent_hash: [0u8; 32],
|
||||
lineage_depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize to a 68-byte array.
|
||||
pub fn to_bytes(&self) -> [u8; 68] {
|
||||
let mut buf = [0u8; 68];
|
||||
buf[0..16].copy_from_slice(&self.file_id);
|
||||
buf[16..32].copy_from_slice(&self.parent_id);
|
||||
buf[32..64].copy_from_slice(&self.parent_hash);
|
||||
buf[64..68].copy_from_slice(&self.lineage_depth.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize from a 68-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 68]) -> Self {
|
||||
let mut file_id = [0u8; 16];
|
||||
file_id.copy_from_slice(&data[0..16]);
|
||||
let mut parent_id = [0u8; 16];
|
||||
parent_id.copy_from_slice(&data[16..32]);
|
||||
let mut parent_hash = [0u8; 32];
|
||||
parent_hash.copy_from_slice(&data[32..64]);
|
||||
// Safety: data is &[u8; 68], so data[64..68] is always exactly 4 bytes.
|
||||
// Use an explicit array conversion to avoid the unwrap.
|
||||
let lineage_depth = u32::from_le_bytes([data[64], data[65], data[66], data[67]]);
|
||||
Self {
|
||||
file_id,
|
||||
parent_id,
|
||||
parent_hash,
|
||||
lineage_depth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A lineage record for witness chain entries.
|
||||
///
|
||||
/// Fixed 128 bytes with a 47-byte description field.
|
||||
///
|
||||
/// Layout:
|
||||
/// | Offset | Size | Field |
|
||||
/// |--------|------|------------------|
|
||||
/// | 0x00 | 16 | file_id |
|
||||
/// | 0x10 | 16 | parent_id |
|
||||
/// | 0x20 | 32 | parent_hash |
|
||||
/// | 0x40 | 1 | derivation_type |
|
||||
/// | 0x41 | 3 | _pad |
|
||||
/// | 0x44 | 4 | mutation_count |
|
||||
/// | 0x48 | 8 | timestamp_ns |
|
||||
/// | 0x50 | 1 | description_len |
|
||||
/// | 0x51 | 47 | description |
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct LineageRecord {
|
||||
/// Unique identifier for this file.
|
||||
pub file_id: [u8; 16],
|
||||
/// Identifier of the parent file.
|
||||
pub parent_id: [u8; 16],
|
||||
/// SHAKE-256-256 hash of the parent's manifest.
|
||||
pub parent_hash: [u8; 32],
|
||||
/// How the child was derived from the parent.
|
||||
pub derivation_type: DerivationType,
|
||||
/// Number of mutations/changes applied.
|
||||
pub mutation_count: u32,
|
||||
/// Nanosecond UNIX timestamp of derivation.
|
||||
pub timestamp_ns: u64,
|
||||
/// Length of the description (max 47).
|
||||
pub description_len: u8,
|
||||
/// UTF-8 description of the derivation (47-byte buffer).
|
||||
pub description: [u8; 47],
|
||||
}
|
||||
|
||||
/// Size of a serialized LineageRecord.
|
||||
pub const LINEAGE_RECORD_SIZE: usize = 128;
|
||||
|
||||
impl LineageRecord {
|
||||
/// Create a new lineage record with a description string.
|
||||
pub fn new(
|
||||
file_id: [u8; 16],
|
||||
parent_id: [u8; 16],
|
||||
parent_hash: [u8; 32],
|
||||
derivation_type: DerivationType,
|
||||
mutation_count: u32,
|
||||
timestamp_ns: u64,
|
||||
desc: &str,
|
||||
) -> Self {
|
||||
let desc_bytes = desc.as_bytes();
|
||||
let desc_len = desc_bytes.len().min(47) as u8;
|
||||
let mut description = [0u8; 47];
|
||||
description[..desc_len as usize].copy_from_slice(&desc_bytes[..desc_len as usize]);
|
||||
Self {
|
||||
file_id,
|
||||
parent_id,
|
||||
parent_hash,
|
||||
derivation_type,
|
||||
mutation_count,
|
||||
timestamp_ns,
|
||||
description_len: desc_len,
|
||||
description,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the description as a string slice.
|
||||
pub fn description_str(&self) -> &str {
|
||||
let len = (self.description_len as usize).min(47);
|
||||
core::str::from_utf8(&self.description[..len]).unwrap_or("")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Witness type constants for lineage entries ----
|
||||
|
||||
/// Witness type: file derivation event.
|
||||
pub const WITNESS_DERIVATION: u8 = 0x09;
|
||||
/// Witness type: lineage merge (multi-parent).
|
||||
pub const WITNESS_LINEAGE_MERGE: u8 = 0x0A;
|
||||
/// Witness type: lineage snapshot.
|
||||
pub const WITNESS_LINEAGE_SNAPSHOT: u8 = 0x0B;
|
||||
/// Witness type: lineage transform.
|
||||
pub const WITNESS_LINEAGE_TRANSFORM: u8 = 0x0C;
|
||||
/// Witness type: lineage verification.
|
||||
pub const WITNESS_LINEAGE_VERIFY: u8 = 0x0D;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn file_identity_size() {
|
||||
assert_eq!(core::mem::size_of::<FileIdentity>(), 68);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_identity_fits_in_reserved() {
|
||||
// Level0Root reserved area is 252 bytes; FileIdentity is 68 bytes
|
||||
assert!(core::mem::size_of::<FileIdentity>() <= 252);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_identity_root() {
|
||||
let id = [0x42u8; 16];
|
||||
let fi = FileIdentity::new_root(id);
|
||||
assert!(fi.is_root());
|
||||
assert_eq!(fi.file_id, id);
|
||||
assert_eq!(fi.parent_id, [0u8; 16]);
|
||||
assert_eq!(fi.parent_hash, [0u8; 32]);
|
||||
assert_eq!(fi.lineage_depth, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_identity_zeroed_is_root() {
|
||||
let fi = FileIdentity::zeroed();
|
||||
assert!(fi.is_root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_identity_round_trip() {
|
||||
let fi = FileIdentity {
|
||||
file_id: [1u8; 16],
|
||||
parent_id: [2u8; 16],
|
||||
parent_hash: [3u8; 32],
|
||||
lineage_depth: 42,
|
||||
};
|
||||
let bytes = fi.to_bytes();
|
||||
let decoded = FileIdentity::from_bytes(&bytes);
|
||||
assert_eq!(fi, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_identity_non_root() {
|
||||
let fi = FileIdentity {
|
||||
file_id: [1u8; 16],
|
||||
parent_id: [2u8; 16],
|
||||
parent_hash: [3u8; 32],
|
||||
lineage_depth: 1,
|
||||
};
|
||||
assert!(!fi.is_root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derivation_type_round_trip() {
|
||||
let cases: &[(u8, DerivationType)] = &[
|
||||
(0, DerivationType::Clone),
|
||||
(1, DerivationType::Filter),
|
||||
(2, DerivationType::Merge),
|
||||
(3, DerivationType::Quantize),
|
||||
(4, DerivationType::Reindex),
|
||||
(5, DerivationType::Transform),
|
||||
(6, DerivationType::Snapshot),
|
||||
(0xFF, DerivationType::UserDefined),
|
||||
];
|
||||
for &(raw, expected) in cases {
|
||||
assert_eq!(DerivationType::try_from(raw), Ok(expected));
|
||||
assert_eq!(expected as u8, raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derivation_type_unknown() {
|
||||
assert_eq!(DerivationType::try_from(7), Err(7));
|
||||
assert_eq!(DerivationType::try_from(0xFE), Err(0xFE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineage_record_description() {
|
||||
let record = LineageRecord::new(
|
||||
[1u8; 16],
|
||||
[2u8; 16],
|
||||
[3u8; 32],
|
||||
DerivationType::Filter,
|
||||
5,
|
||||
1_000_000_000,
|
||||
"filtered by category",
|
||||
);
|
||||
assert_eq!(record.description_str(), "filtered by category");
|
||||
assert_eq!(record.description_len, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineage_record_long_description_truncated() {
|
||||
let long_desc = "a]".repeat(50); // 100 chars, way over 47
|
||||
let record = LineageRecord::new(
|
||||
[0u8; 16],
|
||||
[0u8; 16],
|
||||
[0u8; 32],
|
||||
DerivationType::Clone,
|
||||
0,
|
||||
0,
|
||||
&long_desc,
|
||||
);
|
||||
assert_eq!(record.description_len, 47);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn witness_type_constants() {
|
||||
assert_eq!(WITNESS_DERIVATION, 0x09);
|
||||
assert_eq!(WITNESS_LINEAGE_MERGE, 0x0A);
|
||||
assert_eq!(WITNESS_LINEAGE_SNAPSHOT, 0x0B);
|
||||
assert_eq!(WITNESS_LINEAGE_TRANSFORM, 0x0C);
|
||||
assert_eq!(WITNESS_LINEAGE_VERIFY, 0x0D);
|
||||
}
|
||||
}
|
||||
336
crates/rvf/rvf-types/src/manifest.rs
Normal file
336
crates/rvf/rvf-types/src/manifest.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
//! Level 0 root manifest and hotset pointer types.
|
||||
//!
|
||||
//! The root manifest is always the last 4096 bytes of the most recent
|
||||
//! MANIFEST_SEG. Its fixed size enables instant location via `seek(EOF - 4096)`.
|
||||
|
||||
use crate::constants::ROOT_MANIFEST_MAGIC;
|
||||
|
||||
/// Inline hotset pointer for HNSW entry points.
|
||||
///
|
||||
/// Offset 0x038 in Level0Root.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct EntrypointPtr {
|
||||
/// Byte offset to the segment containing HNSW entry points.
|
||||
pub seg_offset: u64,
|
||||
/// Block offset within that segment.
|
||||
pub block_offset: u32,
|
||||
/// Number of entry points.
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
/// Inline hotset pointer for top-layer adjacency.
|
||||
///
|
||||
/// Offset 0x048 in Level0Root.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct TopLayerPtr {
|
||||
/// Byte offset to the segment with top-layer adjacency.
|
||||
pub seg_offset: u64,
|
||||
/// Block offset within the segment.
|
||||
pub block_offset: u32,
|
||||
/// Number of nodes in the top layer.
|
||||
pub node_count: u32,
|
||||
}
|
||||
|
||||
/// Inline hotset pointer for cluster centroids / pivots.
|
||||
///
|
||||
/// Offset 0x058 in Level0Root.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct CentroidPtr {
|
||||
/// Byte offset to the segment with cluster centroids.
|
||||
pub seg_offset: u64,
|
||||
/// Block offset within the segment.
|
||||
pub block_offset: u32,
|
||||
/// Number of centroids.
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
/// Inline hotset pointer for quantization dictionary.
|
||||
///
|
||||
/// Offset 0x068 in Level0Root.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct QuantDictPtr {
|
||||
/// Byte offset to the quantization dictionary segment.
|
||||
pub seg_offset: u64,
|
||||
/// Block offset within the segment.
|
||||
pub block_offset: u32,
|
||||
/// Dictionary size in bytes.
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
/// Inline hotset pointer for the hot vector cache (HOT_SEG).
|
||||
///
|
||||
/// Offset 0x078 in Level0Root.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct HotCachePtr {
|
||||
/// Byte offset to the HOT_SEG with interleaved hot vectors.
|
||||
pub seg_offset: u64,
|
||||
/// Block offset within the segment.
|
||||
pub block_offset: u32,
|
||||
/// Number of vectors in the hot cache.
|
||||
pub vector_count: u32,
|
||||
}
|
||||
|
||||
/// Inline hotset pointer for prefetch hint table.
|
||||
///
|
||||
/// Offset 0x088 in Level0Root.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct PrefetchMapPtr {
|
||||
/// Byte offset to the prefetch hint table.
|
||||
pub offset: u64,
|
||||
/// Number of prefetch entries.
|
||||
pub entries: u32,
|
||||
/// Padding to align to 16 bytes (matches other hotset pointers).
|
||||
pub _pad: u32,
|
||||
}
|
||||
|
||||
/// The Level 0 root manifest (exactly 4096 bytes).
|
||||
///
|
||||
/// Always located at the last 4096 bytes of the most recent MANIFEST_SEG.
|
||||
/// Its fixed size enables instant boot: `seek(EOF - 4096)`.
|
||||
///
|
||||
/// ## Binary layout
|
||||
///
|
||||
/// | Offset | Size | Field |
|
||||
/// |--------|------|-------|
|
||||
/// | 0x000 | 4 | magic (0x52564D30 "RVM0") |
|
||||
/// | 0x004 | 2 | version |
|
||||
/// | 0x006 | 2 | flags |
|
||||
/// | 0x008 | 8 | l1_manifest_offset |
|
||||
/// | 0x010 | 8 | l1_manifest_length |
|
||||
/// | 0x018 | 8 | total_vector_count |
|
||||
/// | 0x020 | 2 | dimension |
|
||||
/// | 0x022 | 1 | base_dtype |
|
||||
/// | 0x023 | 1 | profile_id |
|
||||
/// | 0x024 | 4 | epoch |
|
||||
/// | 0x028 | 8 | created_ns |
|
||||
/// | 0x030 | 8 | modified_ns |
|
||||
/// | 0x038 | 16 | entrypoint_ptr |
|
||||
/// | 0x048 | 16 | toplayer_ptr |
|
||||
/// | 0x058 | 16 | centroid_ptr |
|
||||
/// | 0x068 | 16 | quantdict_ptr |
|
||||
/// | 0x078 | 16 | hot_cache_ptr |
|
||||
/// | 0x088 | 16 | prefetch_map_ptr (includes 4B padding) |
|
||||
/// | 0x098 | 2 | sig_algo |
|
||||
/// | 0x09A | 2 | sig_length |
|
||||
/// | 0x09C | 3684 | signature_buf |
|
||||
/// | 0xF00 | 252 | reserved |
|
||||
/// | 0xFFC | 4 | root_checksum |
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct Level0Root {
|
||||
// ---- Basic header (0x000 - 0x037) ----
|
||||
/// Magic number: must be `0x52564D30` ("RVM0").
|
||||
pub magic: u32,
|
||||
/// Root manifest version.
|
||||
pub version: u16,
|
||||
/// Root manifest flags.
|
||||
pub flags: u16,
|
||||
/// Byte offset to the Level 1 manifest segment.
|
||||
pub l1_manifest_offset: u64,
|
||||
/// Byte length of the Level 1 manifest segment.
|
||||
pub l1_manifest_length: u64,
|
||||
/// Total vectors across all segments.
|
||||
pub total_vector_count: u64,
|
||||
/// Vector dimensionality.
|
||||
pub dimension: u16,
|
||||
/// Base data type enum (see `DataType`).
|
||||
pub base_dtype: u8,
|
||||
/// Domain profile id.
|
||||
pub profile_id: u8,
|
||||
/// Current overlay epoch number.
|
||||
pub epoch: u32,
|
||||
/// File creation timestamp (nanoseconds).
|
||||
pub created_ns: u64,
|
||||
/// Last modification timestamp (nanoseconds).
|
||||
pub modified_ns: u64,
|
||||
|
||||
// ---- Hotset pointers (0x038 - 0x093) ----
|
||||
/// HNSW entry points.
|
||||
pub entrypoint: EntrypointPtr,
|
||||
/// Top-layer adjacency.
|
||||
pub toplayer: TopLayerPtr,
|
||||
/// Cluster centroids / pivots.
|
||||
pub centroid: CentroidPtr,
|
||||
/// Quantization dictionary.
|
||||
pub quantdict: QuantDictPtr,
|
||||
/// Hot vector cache (HOT_SEG).
|
||||
pub hot_cache: HotCachePtr,
|
||||
/// Prefetch hint table.
|
||||
pub prefetch_map: PrefetchMapPtr,
|
||||
|
||||
// ---- Crypto (0x094 - 0x097 + signature) ----
|
||||
/// Manifest signature algorithm.
|
||||
pub sig_algo: u16,
|
||||
/// Signature byte length.
|
||||
pub sig_length: u16,
|
||||
/// Signature bytes (up to 3688 bytes; only first `sig_length` are meaningful).
|
||||
pub signature_buf: [u8; Self::SIG_BUF_SIZE],
|
||||
|
||||
// ---- Reserved + checksum (0xF00 - 0xFFF) ----
|
||||
/// Reserved / zero-padded area.
|
||||
pub reserved: [u8; 252],
|
||||
/// CRC32C of bytes 0x000 through 0xFFB.
|
||||
pub root_checksum: u32,
|
||||
}
|
||||
|
||||
// Compile-time assertion: Level0Root must be exactly 4096 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<Level0Root>() == 4096);
|
||||
|
||||
impl Level0Root {
|
||||
/// Size of the signature buffer within the root manifest.
|
||||
/// From offset 0x09C to 0xEFF inclusive = 3684 bytes.
|
||||
pub const SIG_BUF_SIZE: usize = 3684;
|
||||
|
||||
/// Create a zeroed root manifest with only the magic set.
|
||||
pub const fn zeroed() -> Self {
|
||||
Self {
|
||||
magic: ROOT_MANIFEST_MAGIC,
|
||||
version: 0,
|
||||
flags: 0,
|
||||
l1_manifest_offset: 0,
|
||||
l1_manifest_length: 0,
|
||||
total_vector_count: 0,
|
||||
dimension: 0,
|
||||
base_dtype: 0,
|
||||
profile_id: 0,
|
||||
epoch: 0,
|
||||
created_ns: 0,
|
||||
modified_ns: 0,
|
||||
entrypoint: EntrypointPtr {
|
||||
seg_offset: 0,
|
||||
block_offset: 0,
|
||||
count: 0,
|
||||
},
|
||||
toplayer: TopLayerPtr {
|
||||
seg_offset: 0,
|
||||
block_offset: 0,
|
||||
node_count: 0,
|
||||
},
|
||||
centroid: CentroidPtr {
|
||||
seg_offset: 0,
|
||||
block_offset: 0,
|
||||
count: 0,
|
||||
},
|
||||
quantdict: QuantDictPtr {
|
||||
seg_offset: 0,
|
||||
block_offset: 0,
|
||||
size: 0,
|
||||
},
|
||||
hot_cache: HotCachePtr {
|
||||
seg_offset: 0,
|
||||
block_offset: 0,
|
||||
vector_count: 0,
|
||||
},
|
||||
prefetch_map: PrefetchMapPtr {
|
||||
offset: 0,
|
||||
entries: 0,
|
||||
_pad: 0,
|
||||
},
|
||||
sig_algo: 0,
|
||||
sig_length: 0,
|
||||
signature_buf: [0u8; Self::SIG_BUF_SIZE],
|
||||
reserved: [0u8; 252],
|
||||
root_checksum: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the magic field matches the expected value.
|
||||
#[inline]
|
||||
pub const fn is_valid_magic(&self) -> bool {
|
||||
self.magic == ROOT_MANIFEST_MAGIC
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn level0_root_size_is_4096() {
|
||||
assert_eq!(core::mem::size_of::<Level0Root>(), 4096);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeroed_has_valid_magic() {
|
||||
let root = Level0Root::zeroed();
|
||||
assert!(root.is_valid_magic());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let root = Level0Root::zeroed();
|
||||
let base = core::ptr::addr_of!(root) as usize;
|
||||
|
||||
// Use addr_of! for packed struct fields to avoid UB.
|
||||
let magic_off = core::ptr::addr_of!(root.magic) as usize - base;
|
||||
let version_off = core::ptr::addr_of!(root.version) as usize - base;
|
||||
let flags_off = core::ptr::addr_of!(root.flags) as usize - base;
|
||||
let l1_offset_off = core::ptr::addr_of!(root.l1_manifest_offset) as usize - base;
|
||||
let l1_length_off = core::ptr::addr_of!(root.l1_manifest_length) as usize - base;
|
||||
let total_vec_off = core::ptr::addr_of!(root.total_vector_count) as usize - base;
|
||||
let dim_off = core::ptr::addr_of!(root.dimension) as usize - base;
|
||||
let dtype_off = core::ptr::addr_of!(root.base_dtype) as usize - base;
|
||||
let profile_off = core::ptr::addr_of!(root.profile_id) as usize - base;
|
||||
let epoch_off = core::ptr::addr_of!(root.epoch) as usize - base;
|
||||
let created_off = core::ptr::addr_of!(root.created_ns) as usize - base;
|
||||
let modified_off = core::ptr::addr_of!(root.modified_ns) as usize - base;
|
||||
let entry_off = core::ptr::addr_of!(root.entrypoint) as usize - base;
|
||||
let toplayer_off = core::ptr::addr_of!(root.toplayer) as usize - base;
|
||||
let centroid_off = core::ptr::addr_of!(root.centroid) as usize - base;
|
||||
let quantdict_off = core::ptr::addr_of!(root.quantdict) as usize - base;
|
||||
let hot_cache_off = core::ptr::addr_of!(root.hot_cache) as usize - base;
|
||||
let prefetch_off = core::ptr::addr_of!(root.prefetch_map) as usize - base;
|
||||
let sig_algo_off = core::ptr::addr_of!(root.sig_algo) as usize - base;
|
||||
let sig_len_off = core::ptr::addr_of!(root.sig_length) as usize - base;
|
||||
let sig_buf_off = core::ptr::addr_of!(root.signature_buf) as usize - base;
|
||||
let reserved_off = core::ptr::addr_of!(root.reserved) as usize - base;
|
||||
let checksum_off = core::ptr::addr_of!(root.root_checksum) as usize - base;
|
||||
|
||||
assert_eq!(magic_off, 0x000);
|
||||
assert_eq!(version_off, 0x004);
|
||||
assert_eq!(flags_off, 0x006);
|
||||
assert_eq!(l1_offset_off, 0x008);
|
||||
assert_eq!(l1_length_off, 0x010);
|
||||
assert_eq!(total_vec_off, 0x018);
|
||||
assert_eq!(dim_off, 0x020);
|
||||
assert_eq!(dtype_off, 0x022);
|
||||
assert_eq!(profile_off, 0x023);
|
||||
assert_eq!(epoch_off, 0x024);
|
||||
assert_eq!(created_off, 0x028);
|
||||
assert_eq!(modified_off, 0x030);
|
||||
assert_eq!(entry_off, 0x038);
|
||||
assert_eq!(toplayer_off, 0x048);
|
||||
assert_eq!(centroid_off, 0x058);
|
||||
assert_eq!(quantdict_off, 0x068);
|
||||
assert_eq!(hot_cache_off, 0x078);
|
||||
assert_eq!(prefetch_off, 0x088);
|
||||
assert_eq!(sig_algo_off, 0x098);
|
||||
assert_eq!(sig_len_off, 0x09A);
|
||||
assert_eq!(sig_buf_off, 0x09C);
|
||||
assert_eq!(reserved_off, 0xF00);
|
||||
assert_eq!(checksum_off, 0xFFC);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hotset_pointer_sizes() {
|
||||
assert_eq!(core::mem::size_of::<EntrypointPtr>(), 16);
|
||||
assert_eq!(core::mem::size_of::<TopLayerPtr>(), 16);
|
||||
assert_eq!(core::mem::size_of::<CentroidPtr>(), 16);
|
||||
assert_eq!(core::mem::size_of::<QuantDictPtr>(), 16);
|
||||
assert_eq!(core::mem::size_of::<HotCachePtr>(), 16);
|
||||
assert_eq!(core::mem::size_of::<PrefetchMapPtr>(), 16);
|
||||
}
|
||||
}
|
||||
277
crates/rvf/rvf-types/src/membership.rs
Normal file
277
crates/rvf/rvf-types/src/membership.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
//! MEMBERSHIP_SEG (0x22) types for the RVF computational container.
|
||||
//!
|
||||
//! Defines the 96-byte `MembershipHeader` and associated enums per ADR-031.
|
||||
//! The MEMBERSHIP_SEG stores vector membership filters for branches,
|
||||
//! tracking which vectors belong to a given snapshot or branch.
|
||||
|
||||
use crate::error::RvfError;
|
||||
|
||||
/// Magic number for `MembershipHeader`: "RVMB" in big-endian.
|
||||
pub const MEMBERSHIP_MAGIC: u32 = 0x5256_4D42;
|
||||
|
||||
/// Filter storage type.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum FilterType {
|
||||
/// Dense bitmap (one bit per vector).
|
||||
Bitmap = 0,
|
||||
/// Roaring bitmap (compressed sparse).
|
||||
RoaringBitmap = 1,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for FilterType {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Bitmap),
|
||||
1 => Ok(Self::RoaringBitmap),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "FilterType",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter mode: include-by-default or exclude-by-default.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum FilterMode {
|
||||
/// Vectors are included unless filtered out.
|
||||
Include = 0,
|
||||
/// Vectors are excluded unless explicitly included.
|
||||
Exclude = 1,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for FilterMode {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Include),
|
||||
1 => Ok(Self::Exclude),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "FilterMode",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 96-byte header for MEMBERSHIP_SEG payloads.
|
||||
///
|
||||
/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
|
||||
/// little-endian on the wire.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct MembershipHeader {
|
||||
/// Magic: `MEMBERSHIP_MAGIC` (0x52564D42, "RVMB").
|
||||
pub magic: u32,
|
||||
/// MembershipHeader format version (currently 1).
|
||||
pub version: u16,
|
||||
/// Filter storage type (see `FilterType`).
|
||||
pub filter_type: u8,
|
||||
/// Filter mode (see `FilterMode`).
|
||||
pub filter_mode: u8,
|
||||
/// Total number of vectors in the dataset.
|
||||
pub vector_count: u64,
|
||||
/// Number of vectors that are members.
|
||||
pub member_count: u64,
|
||||
/// Offset to the membership filter within the segment payload.
|
||||
pub filter_offset: u64,
|
||||
/// Size of the membership filter in bytes.
|
||||
pub filter_size: u32,
|
||||
/// Generation counter for optimistic concurrency.
|
||||
pub generation_id: u32,
|
||||
/// SHAKE-256-256 hash of the filter data.
|
||||
pub filter_hash: [u8; 32],
|
||||
/// Offset to optional Bloom filter for fast negative lookups.
|
||||
pub bloom_offset: u64,
|
||||
/// Size of the Bloom filter in bytes.
|
||||
pub bloom_size: u32,
|
||||
/// Reserved (must be zero).
|
||||
pub _reserved: u32,
|
||||
/// Reserved (must be zero).
|
||||
pub _reserved2: [u8; 8],
|
||||
}
|
||||
|
||||
// Compile-time assertion: MembershipHeader must be exactly 96 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<MembershipHeader>() == 96);
|
||||
|
||||
impl MembershipHeader {
|
||||
/// Serialize the header to a 96-byte little-endian array.
|
||||
pub fn to_bytes(&self) -> [u8; 96] {
|
||||
let mut buf = [0u8; 96];
|
||||
buf[0x00..0x04].copy_from_slice(&self.magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.version.to_le_bytes());
|
||||
buf[0x06] = self.filter_type;
|
||||
buf[0x07] = self.filter_mode;
|
||||
buf[0x08..0x10].copy_from_slice(&self.vector_count.to_le_bytes());
|
||||
buf[0x10..0x18].copy_from_slice(&self.member_count.to_le_bytes());
|
||||
buf[0x18..0x20].copy_from_slice(&self.filter_offset.to_le_bytes());
|
||||
buf[0x20..0x24].copy_from_slice(&self.filter_size.to_le_bytes());
|
||||
buf[0x24..0x28].copy_from_slice(&self.generation_id.to_le_bytes());
|
||||
buf[0x28..0x48].copy_from_slice(&self.filter_hash);
|
||||
buf[0x48..0x50].copy_from_slice(&self.bloom_offset.to_le_bytes());
|
||||
buf[0x50..0x54].copy_from_slice(&self.bloom_size.to_le_bytes());
|
||||
buf[0x54..0x58].copy_from_slice(&self._reserved.to_le_bytes());
|
||||
buf[0x58..0x60].copy_from_slice(&self._reserved2);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize a `MembershipHeader` from a 96-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 96]) -> Result<Self, RvfError> {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != MEMBERSHIP_MAGIC {
|
||||
return Err(RvfError::BadMagic {
|
||||
expected: MEMBERSHIP_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version: u16::from_le_bytes([data[0x04], data[0x05]]),
|
||||
filter_type: data[0x06],
|
||||
filter_mode: data[0x07],
|
||||
vector_count: u64::from_le_bytes([
|
||||
data[0x08], data[0x09], data[0x0A], data[0x0B], data[0x0C], data[0x0D], data[0x0E],
|
||||
data[0x0F],
|
||||
]),
|
||||
member_count: u64::from_le_bytes([
|
||||
data[0x10], data[0x11], data[0x12], data[0x13], data[0x14], data[0x15], data[0x16],
|
||||
data[0x17],
|
||||
]),
|
||||
filter_offset: u64::from_le_bytes([
|
||||
data[0x18], data[0x19], data[0x1A], data[0x1B], data[0x1C], data[0x1D], data[0x1E],
|
||||
data[0x1F],
|
||||
]),
|
||||
filter_size: u32::from_le_bytes([data[0x20], data[0x21], data[0x22], data[0x23]]),
|
||||
generation_id: u32::from_le_bytes([data[0x24], data[0x25], data[0x26], data[0x27]]),
|
||||
filter_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x28..0x48]);
|
||||
h
|
||||
},
|
||||
bloom_offset: u64::from_le_bytes([
|
||||
data[0x48], data[0x49], data[0x4A], data[0x4B], data[0x4C], data[0x4D], data[0x4E],
|
||||
data[0x4F],
|
||||
]),
|
||||
bloom_size: u32::from_le_bytes([data[0x50], data[0x51], data[0x52], data[0x53]]),
|
||||
_reserved: u32::from_le_bytes([data[0x54], data[0x55], data[0x56], data[0x57]]),
|
||||
_reserved2: {
|
||||
let mut r = [0u8; 8];
|
||||
r.copy_from_slice(&data[0x58..0x60]);
|
||||
r
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_header() -> MembershipHeader {
|
||||
MembershipHeader {
|
||||
magic: MEMBERSHIP_MAGIC,
|
||||
version: 1,
|
||||
filter_type: FilterType::Bitmap as u8,
|
||||
filter_mode: FilterMode::Include as u8,
|
||||
vector_count: 1_000_000,
|
||||
member_count: 500_000,
|
||||
filter_offset: 96,
|
||||
filter_size: 125_000,
|
||||
generation_id: 1,
|
||||
filter_hash: [0xCC; 32],
|
||||
bloom_offset: 0,
|
||||
bloom_size: 0,
|
||||
_reserved: 0,
|
||||
_reserved2: [0; 8],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_96() {
|
||||
assert_eq!(core::mem::size_of::<MembershipHeader>(), 96);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
let bytes_be = MEMBERSHIP_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVMB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_header();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = MembershipHeader::from_bytes(&bytes).expect("from_bytes should succeed");
|
||||
|
||||
assert_eq!(decoded.magic, MEMBERSHIP_MAGIC);
|
||||
assert_eq!(decoded.version, 1);
|
||||
assert_eq!(decoded.filter_type, FilterType::Bitmap as u8);
|
||||
assert_eq!(decoded.filter_mode, FilterMode::Include as u8);
|
||||
assert_eq!(decoded.vector_count, 1_000_000);
|
||||
assert_eq!(decoded.member_count, 500_000);
|
||||
assert_eq!(decoded.filter_offset, 96);
|
||||
assert_eq!(decoded.filter_size, 125_000);
|
||||
assert_eq!(decoded.generation_id, 1);
|
||||
assert_eq!(decoded.filter_hash, [0xCC; 32]);
|
||||
assert_eq!(decoded.bloom_offset, 0);
|
||||
assert_eq!(decoded.bloom_size, 0);
|
||||
assert_eq!(decoded._reserved, 0);
|
||||
assert_eq!(decoded._reserved2, [0; 8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_magic_returns_error() {
|
||||
let mut bytes = sample_header().to_bytes();
|
||||
bytes[0] = 0x00; // corrupt magic
|
||||
let err = MembershipHeader::from_bytes(&bytes).unwrap_err();
|
||||
match err {
|
||||
RvfError::BadMagic { expected, .. } => assert_eq!(expected, MEMBERSHIP_MAGIC),
|
||||
other => panic!("expected BadMagic, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let h = sample_header();
|
||||
let base = &h as *const _ as usize;
|
||||
|
||||
assert_eq!(&h.magic as *const _ as usize - base, 0x00);
|
||||
assert_eq!(&h.version as *const _ as usize - base, 0x04);
|
||||
assert_eq!(&h.filter_type as *const _ as usize - base, 0x06);
|
||||
assert_eq!(&h.filter_mode as *const _ as usize - base, 0x07);
|
||||
assert_eq!(&h.vector_count as *const _ as usize - base, 0x08);
|
||||
assert_eq!(&h.member_count as *const _ as usize - base, 0x10);
|
||||
assert_eq!(&h.filter_offset as *const _ as usize - base, 0x18);
|
||||
assert_eq!(&h.filter_size as *const _ as usize - base, 0x20);
|
||||
assert_eq!(&h.generation_id as *const _ as usize - base, 0x24);
|
||||
assert_eq!(&h.filter_hash as *const _ as usize - base, 0x28);
|
||||
assert_eq!(&h.bloom_offset as *const _ as usize - base, 0x48);
|
||||
assert_eq!(&h.bloom_size as *const _ as usize - base, 0x50);
|
||||
assert_eq!(&h._reserved as *const _ as usize - base, 0x54);
|
||||
assert_eq!(&h._reserved2 as *const _ as usize - base, 0x58);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_type_try_from() {
|
||||
assert_eq!(FilterType::try_from(0), Ok(FilterType::Bitmap));
|
||||
assert_eq!(FilterType::try_from(1), Ok(FilterType::RoaringBitmap));
|
||||
assert!(FilterType::try_from(2).is_err());
|
||||
assert!(FilterType::try_from(0xFF).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_mode_try_from() {
|
||||
assert_eq!(FilterMode::try_from(0), Ok(FilterMode::Include));
|
||||
assert_eq!(FilterMode::try_from(1), Ok(FilterMode::Exclude));
|
||||
assert!(FilterMode::try_from(2).is_err());
|
||||
assert!(FilterMode::try_from(0xFF).is_err());
|
||||
}
|
||||
}
|
||||
190
crates/rvf/rvf-types/src/profile.rs
Normal file
190
crates/rvf/rvf-types/src/profile.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
//! Hardware and domain profile identifiers.
|
||||
|
||||
/// Hardware profile ID (stored in root manifest `profile_id` for hardware tier).
|
||||
///
|
||||
/// Determines the runtime behaviour profile (memory budget, tier policy, etc.).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum ProfileId {
|
||||
/// Generic / minimal profile.
|
||||
Generic = 0,
|
||||
/// Core profile (moderate resources).
|
||||
Core = 1,
|
||||
/// Hot profile (high-performance, memory-rich).
|
||||
Hot = 2,
|
||||
/// Full profile (all features enabled).
|
||||
Full = 3,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ProfileId {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Generic),
|
||||
1 => Ok(Self::Core),
|
||||
2 => Ok(Self::Hot),
|
||||
3 => Ok(Self::Full),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Domain profile discriminator (semantic overlay on the RVF substrate).
|
||||
///
|
||||
/// Stored in the root manifest `profile_id` field and declared in PROFILE_SEG.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum DomainProfile {
|
||||
/// Generic / unspecified domain.
|
||||
Generic = 0,
|
||||
/// Genomics (RVDNA) -- codon, k-mer, motif, structure embeddings.
|
||||
Rvdna = 1,
|
||||
/// Language / text (RVText) -- sentence, paragraph, document embeddings.
|
||||
RvText = 2,
|
||||
/// Graph / network (RVGraph) -- node, edge, subgraph embeddings.
|
||||
RvGraph = 3,
|
||||
/// Vision / imagery (RVVision) -- patch, image, object embeddings.
|
||||
RvVision = 4,
|
||||
}
|
||||
|
||||
impl DomainProfile {
|
||||
/// The 4-byte magic number associated with each domain profile.
|
||||
pub const fn magic(self) -> u32 {
|
||||
match self {
|
||||
Self::Generic => 0x0000_0000,
|
||||
Self::Rvdna => 0x5244_4E41, // "RDNA"
|
||||
Self::RvText => 0x5254_5854, // "RTXT"
|
||||
Self::RvGraph => 0x5247_5248, // "RGRH"
|
||||
Self::RvVision => 0x5256_4953, // "RVIS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DomainProfile {
|
||||
/// The canonical file extension for this domain profile.
|
||||
pub const fn extension(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Generic => "rvf",
|
||||
Self::Rvdna => "rvdna",
|
||||
Self::RvText => "rvtext",
|
||||
Self::RvGraph => "rvgraph",
|
||||
Self::RvVision => "rvvis",
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a domain profile from a file extension (case-insensitive).
|
||||
pub fn from_extension(ext: &str) -> Option<Self> {
|
||||
// Manual case-insensitive comparison for no_std compatibility
|
||||
let ext_bytes = ext.as_bytes();
|
||||
if eq_ignore_ascii_case(ext_bytes, b"rvf") {
|
||||
Some(Self::Generic)
|
||||
} else if eq_ignore_ascii_case(ext_bytes, b"rvdna") {
|
||||
Some(Self::Rvdna)
|
||||
} else if eq_ignore_ascii_case(ext_bytes, b"rvtext") {
|
||||
Some(Self::RvText)
|
||||
} else if eq_ignore_ascii_case(ext_bytes, b"rvgraph") {
|
||||
Some(Self::RvGraph)
|
||||
} else if eq_ignore_ascii_case(ext_bytes, b"rvvis") {
|
||||
Some(Self::RvVision)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Case-insensitive ASCII byte comparison.
|
||||
fn eq_ignore_ascii_case(a: &[u8], b: &[u8]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
a.iter()
|
||||
.zip(b.iter())
|
||||
.all(|(x, y)| x.eq_ignore_ascii_case(y))
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for DomainProfile {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Generic),
|
||||
1 => Ok(Self::Rvdna),
|
||||
2 => Ok(Self::RvText),
|
||||
3 => Ok(Self::RvGraph),
|
||||
4 => Ok(Self::RvVision),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn profile_id_round_trip() {
|
||||
for raw in 0..=3u8 {
|
||||
let p = ProfileId::try_from(raw).unwrap();
|
||||
assert_eq!(p as u8, raw);
|
||||
}
|
||||
assert_eq!(ProfileId::try_from(4), Err(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_profile_round_trip() {
|
||||
for raw in 0..=4u8 {
|
||||
let d = DomainProfile::try_from(raw).unwrap();
|
||||
assert_eq!(d as u8, raw);
|
||||
}
|
||||
assert_eq!(DomainProfile::try_from(5), Err(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_extension_round_trip() {
|
||||
let profiles = [
|
||||
DomainProfile::Generic,
|
||||
DomainProfile::Rvdna,
|
||||
DomainProfile::RvText,
|
||||
DomainProfile::RvGraph,
|
||||
DomainProfile::RvVision,
|
||||
];
|
||||
for p in profiles {
|
||||
let ext = p.extension();
|
||||
let back = DomainProfile::from_extension(ext).unwrap();
|
||||
assert_eq!(back, p, "round-trip failed for {ext}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_extension_case_insensitive() {
|
||||
assert_eq!(
|
||||
DomainProfile::from_extension("RVDNA"),
|
||||
Some(DomainProfile::Rvdna)
|
||||
);
|
||||
assert_eq!(
|
||||
DomainProfile::from_extension("RvF"),
|
||||
Some(DomainProfile::Generic)
|
||||
);
|
||||
assert_eq!(
|
||||
DomainProfile::from_extension("RvText"),
|
||||
Some(DomainProfile::RvText)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_extension_unknown() {
|
||||
assert_eq!(DomainProfile::from_extension("txt"), None);
|
||||
assert_eq!(DomainProfile::from_extension(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_magic_values() {
|
||||
assert_eq!(&DomainProfile::Rvdna.magic().to_be_bytes(), b"RDNA");
|
||||
assert_eq!(&DomainProfile::RvText.magic().to_be_bytes(), b"RTXT");
|
||||
assert_eq!(&DomainProfile::RvGraph.magic().to_be_bytes(), b"RGRH");
|
||||
assert_eq!(&DomainProfile::RvVision.magic().to_be_bytes(), b"RVIS");
|
||||
}
|
||||
}
|
||||
378
crates/rvf/rvf-types/src/qr_seed.rs
Normal file
378
crates/rvf/rvf-types/src/qr_seed.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
//! QR Cognitive Seed types for ADR-034.
|
||||
//!
|
||||
//! Defines the RVQS (RuVector QR Seed) binary format — a compact
|
||||
//! self-bootstrapping cognitive payload that fits in a single QR code.
|
||||
//! Scan and mount a portable brain.
|
||||
|
||||
/// RVQS magic: "RVQS" in ASCII = 0x52565153.
|
||||
pub const SEED_MAGIC: u32 = 0x5256_5153;
|
||||
|
||||
/// Maximum payload that fits in QR Version 40, Low EC.
|
||||
pub const QR_MAX_BYTES: usize = 2_953;
|
||||
|
||||
// ---- Seed Flags (bit positions) ----
|
||||
|
||||
/// Embedded WASM microkernel present.
|
||||
pub const SEED_HAS_MICROKERNEL: u16 = 0x0001;
|
||||
/// Progressive download manifest present.
|
||||
pub const SEED_HAS_DOWNLOAD: u16 = 0x0002;
|
||||
/// Payload is signed.
|
||||
pub const SEED_SIGNED: u16 = 0x0004;
|
||||
/// Seed is useful without network access.
|
||||
pub const SEED_OFFLINE_CAPABLE: u16 = 0x0008;
|
||||
/// Payload is encrypted.
|
||||
pub const SEED_ENCRYPTED: u16 = 0x0010;
|
||||
/// Microkernel is Brotli-compressed.
|
||||
pub const SEED_COMPRESSED: u16 = 0x0020;
|
||||
/// Seed contains inline vector data.
|
||||
pub const SEED_HAS_VECTORS: u16 = 0x0040;
|
||||
/// Seed can upgrade itself via streaming.
|
||||
pub const SEED_STREAM_UPGRADE: u16 = 0x0080;
|
||||
|
||||
/// Header size in bytes (fixed).
|
||||
pub const SEED_HEADER_SIZE: usize = 64;
|
||||
|
||||
/// RVQS header — the first 64 bytes of any QR Cognitive Seed.
|
||||
///
|
||||
/// Contains everything needed to verify and bootstrap the seed
|
||||
/// before any network access.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct SeedHeader {
|
||||
/// Magic number: must be SEED_MAGIC.
|
||||
pub seed_magic: u32,
|
||||
/// Seed format version.
|
||||
pub seed_version: u16,
|
||||
/// Seed flags bitfield.
|
||||
pub flags: u16,
|
||||
/// Unique identifier for this seed.
|
||||
pub file_id: [u8; 8],
|
||||
/// Expected total vectors when fully loaded.
|
||||
pub total_vector_count: u32,
|
||||
/// Vector dimensionality.
|
||||
pub dimension: u16,
|
||||
/// Base data type (DataType enum).
|
||||
pub base_dtype: u8,
|
||||
/// Domain profile id.
|
||||
pub profile_id: u8,
|
||||
/// Seed creation timestamp (nanoseconds since epoch).
|
||||
pub created_ns: u64,
|
||||
/// Offset to WASM microkernel data within the seed payload.
|
||||
pub microkernel_offset: u32,
|
||||
/// Compressed microkernel size in bytes.
|
||||
pub microkernel_size: u32,
|
||||
/// Offset to download manifest within the seed payload.
|
||||
pub download_manifest_offset: u32,
|
||||
/// Download manifest size in bytes.
|
||||
pub download_manifest_size: u32,
|
||||
/// Signature algorithm (0=Ed25519, 1=ML-DSA-65).
|
||||
pub sig_algo: u16,
|
||||
/// Signature byte length.
|
||||
pub sig_length: u16,
|
||||
/// Total seed payload size in bytes.
|
||||
pub total_seed_size: u32,
|
||||
/// SHAKE-256-64 of the complete expanded RVF file.
|
||||
pub content_hash: [u8; 8],
|
||||
}
|
||||
|
||||
const _: () = assert!(core::mem::size_of::<SeedHeader>() == SEED_HEADER_SIZE);
|
||||
|
||||
impl SeedHeader {
|
||||
/// Check if the magic field is valid.
|
||||
pub const fn is_valid_magic(&self) -> bool {
|
||||
self.seed_magic == SEED_MAGIC
|
||||
}
|
||||
|
||||
/// Check if the seed has an embedded microkernel.
|
||||
pub const fn has_microkernel(&self) -> bool {
|
||||
self.flags & SEED_HAS_MICROKERNEL != 0
|
||||
}
|
||||
|
||||
/// Check if the seed has a download manifest.
|
||||
pub const fn has_download_manifest(&self) -> bool {
|
||||
self.flags & SEED_HAS_DOWNLOAD != 0
|
||||
}
|
||||
|
||||
/// Check if the seed is signed.
|
||||
pub const fn is_signed(&self) -> bool {
|
||||
self.flags & SEED_SIGNED != 0
|
||||
}
|
||||
|
||||
/// Check if the seed is offline-capable.
|
||||
pub const fn is_offline_capable(&self) -> bool {
|
||||
self.flags & SEED_OFFLINE_CAPABLE != 0
|
||||
}
|
||||
|
||||
/// Check if the seed fits in a single QR code.
|
||||
pub const fn fits_in_qr(&self) -> bool {
|
||||
(self.total_seed_size as usize) <= QR_MAX_BYTES
|
||||
}
|
||||
|
||||
/// Serialize the header to 64 bytes (little-endian).
|
||||
pub fn to_bytes(&self) -> [u8; SEED_HEADER_SIZE] {
|
||||
let mut buf = [0u8; SEED_HEADER_SIZE];
|
||||
buf[0x00..0x04].copy_from_slice(&self.seed_magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.seed_version.to_le_bytes());
|
||||
buf[0x06..0x08].copy_from_slice(&self.flags.to_le_bytes());
|
||||
buf[0x08..0x10].copy_from_slice(&self.file_id);
|
||||
buf[0x10..0x14].copy_from_slice(&self.total_vector_count.to_le_bytes());
|
||||
buf[0x14..0x16].copy_from_slice(&self.dimension.to_le_bytes());
|
||||
buf[0x16] = self.base_dtype;
|
||||
buf[0x17] = self.profile_id;
|
||||
buf[0x18..0x20].copy_from_slice(&self.created_ns.to_le_bytes());
|
||||
buf[0x20..0x24].copy_from_slice(&self.microkernel_offset.to_le_bytes());
|
||||
buf[0x24..0x28].copy_from_slice(&self.microkernel_size.to_le_bytes());
|
||||
buf[0x28..0x2C].copy_from_slice(&self.download_manifest_offset.to_le_bytes());
|
||||
buf[0x2C..0x30].copy_from_slice(&self.download_manifest_size.to_le_bytes());
|
||||
buf[0x30..0x32].copy_from_slice(&self.sig_algo.to_le_bytes());
|
||||
buf[0x32..0x34].copy_from_slice(&self.sig_length.to_le_bytes());
|
||||
buf[0x34..0x38].copy_from_slice(&self.total_seed_size.to_le_bytes());
|
||||
buf[0x38..0x40].copy_from_slice(&self.content_hash);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize from 64 bytes (little-endian).
|
||||
pub fn from_bytes(buf: &[u8]) -> Result<Self, crate::error::RvfError> {
|
||||
if buf.len() < SEED_HEADER_SIZE {
|
||||
return Err(crate::error::RvfError::SizeMismatch {
|
||||
expected: SEED_HEADER_SIZE,
|
||||
got: buf.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let seed_magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
||||
if seed_magic != SEED_MAGIC {
|
||||
return Err(crate::error::RvfError::BadMagic {
|
||||
expected: SEED_MAGIC,
|
||||
got: seed_magic,
|
||||
});
|
||||
}
|
||||
|
||||
let mut file_id = [0u8; 8];
|
||||
file_id.copy_from_slice(&buf[0x08..0x10]);
|
||||
let mut content_hash = [0u8; 8];
|
||||
content_hash.copy_from_slice(&buf[0x38..0x40]);
|
||||
|
||||
Ok(Self {
|
||||
seed_magic,
|
||||
seed_version: u16::from_le_bytes([buf[0x04], buf[0x05]]),
|
||||
flags: u16::from_le_bytes([buf[0x06], buf[0x07]]),
|
||||
file_id,
|
||||
total_vector_count: u32::from_le_bytes([buf[0x10], buf[0x11], buf[0x12], buf[0x13]]),
|
||||
dimension: u16::from_le_bytes([buf[0x14], buf[0x15]]),
|
||||
base_dtype: buf[0x16],
|
||||
profile_id: buf[0x17],
|
||||
created_ns: u64::from_le_bytes([
|
||||
buf[0x18], buf[0x19], buf[0x1A], buf[0x1B], buf[0x1C], buf[0x1D], buf[0x1E],
|
||||
buf[0x1F],
|
||||
]),
|
||||
microkernel_offset: u32::from_le_bytes([buf[0x20], buf[0x21], buf[0x22], buf[0x23]]),
|
||||
microkernel_size: u32::from_le_bytes([buf[0x24], buf[0x25], buf[0x26], buf[0x27]]),
|
||||
download_manifest_offset: u32::from_le_bytes([
|
||||
buf[0x28], buf[0x29], buf[0x2A], buf[0x2B],
|
||||
]),
|
||||
download_manifest_size: u32::from_le_bytes([
|
||||
buf[0x2C], buf[0x2D], buf[0x2E], buf[0x2F],
|
||||
]),
|
||||
sig_algo: u16::from_le_bytes([buf[0x30], buf[0x31]]),
|
||||
sig_length: u16::from_le_bytes([buf[0x32], buf[0x33]]),
|
||||
total_seed_size: u32::from_le_bytes([buf[0x34], buf[0x35], buf[0x36], buf[0x37]]),
|
||||
content_hash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Download Manifest TLV Tags ----
|
||||
|
||||
/// Primary download host.
|
||||
pub const DL_TAG_HOST_PRIMARY: u16 = 0x0001;
|
||||
/// Fallback download host.
|
||||
pub const DL_TAG_HOST_FALLBACK: u16 = 0x0002;
|
||||
/// SHAKE-256-256 hash of the full RVF file.
|
||||
pub const DL_TAG_CONTENT_HASH: u16 = 0x0003;
|
||||
/// Expected total file size.
|
||||
pub const DL_TAG_TOTAL_SIZE: u16 = 0x0004;
|
||||
/// Progressive layer manifest.
|
||||
pub const DL_TAG_LAYER_MANIFEST: u16 = 0x0005;
|
||||
/// Ephemeral session token.
|
||||
pub const DL_TAG_SESSION_TOKEN: u16 = 0x0006;
|
||||
/// Token TTL in seconds.
|
||||
pub const DL_TAG_TTL: u16 = 0x0007;
|
||||
/// TLS certificate pin (SHA-256 of SPKI).
|
||||
pub const DL_TAG_CERT_PIN: u16 = 0x0008;
|
||||
|
||||
/// A single host entry in the download manifest.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct HostEntry {
|
||||
/// Download URL (HTTPS).
|
||||
pub url: [u8; 128],
|
||||
/// Actual URL length within the buffer.
|
||||
pub url_length: u16,
|
||||
/// Priority (lower = preferred).
|
||||
pub priority: u16,
|
||||
/// Geographic region hint.
|
||||
pub region: u16,
|
||||
/// SHAKE-256-128 of host's public key.
|
||||
pub host_key_hash: [u8; 16],
|
||||
}
|
||||
|
||||
impl HostEntry {
|
||||
/// Get the URL as a string slice.
|
||||
pub fn url_str(&self) -> Option<&str> {
|
||||
core::str::from_utf8(&self.url[..self.url_length as usize]).ok()
|
||||
}
|
||||
|
||||
/// Encode to bytes.
|
||||
pub fn to_bytes(&self) -> [u8; 150] {
|
||||
let mut buf = [0u8; 150];
|
||||
buf[0..2].copy_from_slice(&self.url_length.to_le_bytes());
|
||||
buf[2..130].copy_from_slice(&self.url);
|
||||
buf[130..132].copy_from_slice(&self.priority.to_le_bytes());
|
||||
buf[132..134].copy_from_slice(&self.region.to_le_bytes());
|
||||
buf[134..150].copy_from_slice(&self.host_key_hash);
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
/// A single layer entry in the progressive download manifest.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct LayerEntry {
|
||||
/// Byte offset in the full RVF file.
|
||||
pub offset: u32,
|
||||
/// Layer size in bytes.
|
||||
pub size: u32,
|
||||
/// SHAKE-256-128 content hash.
|
||||
pub content_hash: [u8; 16],
|
||||
/// Layer identifier.
|
||||
pub layer_id: u8,
|
||||
/// Download priority (0 = immediate).
|
||||
pub priority: u8,
|
||||
/// 1 = required before first query.
|
||||
pub required: u8,
|
||||
/// Padding.
|
||||
pub _pad: u8,
|
||||
}
|
||||
|
||||
const _: () = assert!(core::mem::size_of::<LayerEntry>() == 28);
|
||||
|
||||
/// Well-known layer identifiers.
|
||||
pub mod layer_id {
|
||||
/// Level 0 manifest (4 KB).
|
||||
pub const LEVEL0: u8 = 0;
|
||||
/// Hot cache (centroids + entry points).
|
||||
pub const HOT_CACHE: u8 = 1;
|
||||
/// HNSW Layer A (recall >= 0.70).
|
||||
pub const HNSW_LAYER_A: u8 = 2;
|
||||
/// Quantization dictionaries.
|
||||
pub const QUANT_DICT: u8 = 3;
|
||||
/// HNSW Layer B (recall >= 0.85).
|
||||
pub const HNSW_LAYER_B: u8 = 4;
|
||||
/// Full vectors (warm tier).
|
||||
pub const FULL_VECTORS: u8 = 5;
|
||||
/// HNSW Layer C (recall >= 0.95).
|
||||
pub const HNSW_LAYER_C: u8 = 6;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
extern crate alloc;
|
||||
|
||||
fn test_header() -> SeedHeader {
|
||||
SeedHeader {
|
||||
seed_magic: SEED_MAGIC,
|
||||
seed_version: 1,
|
||||
flags: SEED_HAS_MICROKERNEL
|
||||
| SEED_HAS_DOWNLOAD
|
||||
| SEED_SIGNED
|
||||
| SEED_COMPRESSED
|
||||
| SEED_STREAM_UPGRADE,
|
||||
file_id: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08],
|
||||
total_vector_count: 100_000,
|
||||
dimension: 384,
|
||||
base_dtype: 1, // F16
|
||||
profile_id: 2, // Hot
|
||||
created_ns: 1_700_000_000_000_000_000,
|
||||
microkernel_offset: 64,
|
||||
microkernel_size: 2100,
|
||||
download_manifest_offset: 2164,
|
||||
download_manifest_size: 512,
|
||||
sig_algo: 0, // Ed25519
|
||||
sig_length: 64,
|
||||
total_seed_size: 2740,
|
||||
content_hash: [0xAB; 8],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_64() {
|
||||
assert_eq!(core::mem::size_of::<SeedHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_entry_size_is_28() {
|
||||
assert_eq!(core::mem::size_of::<LayerEntry>(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_round_trip() {
|
||||
let header = test_header();
|
||||
let bytes = header.to_bytes();
|
||||
let decoded = SeedHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(header, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_invalid_magic() {
|
||||
let mut bytes = test_header().to_bytes();
|
||||
bytes[0] = 0xFF; // Corrupt magic.
|
||||
assert!(SeedHeader::from_bytes(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_too_short() {
|
||||
let bytes = [0u8; 32];
|
||||
assert!(SeedHeader::from_bytes(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_flags() {
|
||||
let header = test_header();
|
||||
assert!(header.has_microkernel());
|
||||
assert!(header.has_download_manifest());
|
||||
assert!(header.is_signed());
|
||||
assert!(!header.is_offline_capable());
|
||||
assert!(header.fits_in_qr());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_fits_in_qr() {
|
||||
let mut header = test_header();
|
||||
header.total_seed_size = 2953; // Max QR capacity.
|
||||
assert!(header.fits_in_qr());
|
||||
header.total_seed_size = 2954;
|
||||
assert!(!header.fits_in_qr());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_magic_is_rvqs() {
|
||||
let bytes = SEED_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes, b"RVQS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_bit_positions() {
|
||||
assert_eq!(SEED_HAS_MICROKERNEL, 1 << 0);
|
||||
assert_eq!(SEED_HAS_DOWNLOAD, 1 << 1);
|
||||
assert_eq!(SEED_SIGNED, 1 << 2);
|
||||
assert_eq!(SEED_OFFLINE_CAPABLE, 1 << 3);
|
||||
assert_eq!(SEED_ENCRYPTED, 1 << 4);
|
||||
assert_eq!(SEED_COMPRESSED, 1 << 5);
|
||||
assert_eq!(SEED_HAS_VECTORS, 1 << 6);
|
||||
assert_eq!(SEED_STREAM_UPGRADE, 1 << 7);
|
||||
}
|
||||
}
|
||||
414
crates/rvf/rvf-types/src/quality.rs
Normal file
414
crates/rvf/rvf-types/src/quality.rs
Normal file
@@ -0,0 +1,414 @@
|
||||
//! Quality envelope types for ADR-033 progressive indexing hardening.
|
||||
//!
|
||||
//! Defines the mandatory outer return type (`QualityEnvelope`) for all query
|
||||
//! APIs, along with retrieval-level and response-level quality signals,
|
||||
//! budget reporting, and degradation diagnostics.
|
||||
|
||||
/// Quality confidence for a single retrieval candidate.
|
||||
///
|
||||
/// Attached per-candidate during the search pipeline. Internal use only;
|
||||
/// consumers see `ResponseQuality` via the `QualityEnvelope`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum RetrievalQuality {
|
||||
/// Full index traversed, high confidence in candidate set.
|
||||
Full = 0x00,
|
||||
/// Partial index (Layer A+B), good confidence.
|
||||
Partial = 0x01,
|
||||
/// Layer A only, moderate confidence.
|
||||
LayerAOnly = 0x02,
|
||||
/// Degenerate distribution detected, low confidence.
|
||||
DegenerateDetected = 0x03,
|
||||
/// Brute-force fallback used within budget, exact over scanned region.
|
||||
BruteForceBudgeted = 0x04,
|
||||
}
|
||||
|
||||
/// Response-level quality signal returned to the caller at the API boundary.
|
||||
///
|
||||
/// This is the field that consumers (RAG pipelines, agent tool chains,
|
||||
/// MCP clients) **must** inspect before using results.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum ResponseQuality {
|
||||
/// All results from full index. Trust fully.
|
||||
Verified = 0x00,
|
||||
/// Results from partial index. Usable but may miss neighbors.
|
||||
Usable = 0x01,
|
||||
/// Degraded retrieval detected. Results are best-effort.
|
||||
Degraded = 0x02,
|
||||
/// Insufficient candidates found. Results are unreliable.
|
||||
Unreliable = 0x03,
|
||||
}
|
||||
|
||||
/// Caller hint for quality vs latency trade-off.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum QualityPreference {
|
||||
/// Runtime decides. Default. Fastest path that meets internal thresholds.
|
||||
Auto = 0x00,
|
||||
/// Caller prefers quality over latency. Runtime may widen n_probe,
|
||||
/// extend budgets up to 4x, and block until Layer B loads.
|
||||
PreferQuality = 0x01,
|
||||
/// Caller prefers latency over quality. Runtime may skip safety net,
|
||||
/// reduce n_probe. ResponseQuality honestly reports what it gets.
|
||||
PreferLatency = 0x02,
|
||||
/// Caller explicitly accepts degraded results. Required to proceed
|
||||
/// when ResponseQuality would be Degraded or Unreliable under Auto.
|
||||
AcceptDegraded = 0x03,
|
||||
}
|
||||
|
||||
impl Default for QualityPreference {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
||||
|
||||
/// Which index layers were available and used during a query.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct IndexLayersUsed {
|
||||
pub layer_a: bool,
|
||||
pub layer_b: bool,
|
||||
pub layer_c: bool,
|
||||
pub hot_cache: bool,
|
||||
}
|
||||
|
||||
/// Evidence chain: what index state was actually used for a query.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SearchEvidenceSummary {
|
||||
/// Which index layers were available and used.
|
||||
pub layers_used: IndexLayersUsed,
|
||||
/// Effective n_probe (after any adaptive widening).
|
||||
pub n_probe_effective: u32,
|
||||
/// Whether degenerate distribution was detected.
|
||||
pub degenerate_detected: bool,
|
||||
/// Coefficient of variation of top-K centroid distances.
|
||||
pub centroid_distance_cv: f32,
|
||||
/// Number of candidates found by HNSW before safety net.
|
||||
pub hnsw_candidate_count: u32,
|
||||
/// Number of candidates added by safety net scan.
|
||||
pub safety_net_candidate_count: u32,
|
||||
}
|
||||
|
||||
impl Default for SearchEvidenceSummary {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
layers_used: IndexLayersUsed::default(),
|
||||
n_probe_effective: 0,
|
||||
degenerate_detected: false,
|
||||
centroid_distance_cv: 0.0,
|
||||
hnsw_candidate_count: 0,
|
||||
safety_net_candidate_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource consumption report for a single query.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct BudgetReport {
|
||||
/// Wall-clock time for centroid routing (microseconds).
|
||||
pub centroid_routing_us: u64,
|
||||
/// Wall-clock time for HNSW traversal (microseconds).
|
||||
pub hnsw_traversal_us: u64,
|
||||
/// Wall-clock time for safety net scan (microseconds).
|
||||
pub safety_net_scan_us: u64,
|
||||
/// Wall-clock time for reranking (microseconds).
|
||||
pub reranking_us: u64,
|
||||
/// Total wall-clock time (microseconds).
|
||||
pub total_us: u64,
|
||||
/// Distance evaluations performed.
|
||||
pub distance_ops: u64,
|
||||
/// Distance evaluations budget.
|
||||
pub distance_ops_budget: u64,
|
||||
/// Bytes read from storage.
|
||||
pub bytes_read: u64,
|
||||
/// Candidates scanned in safety net.
|
||||
pub linear_scan_count: u64,
|
||||
/// Candidate scan budget.
|
||||
pub linear_scan_budget: u64,
|
||||
}
|
||||
|
||||
/// Which fallback path was chosen during query execution.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum FallbackPath {
|
||||
/// Normal HNSW traversal, no fallback needed.
|
||||
None = 0x00,
|
||||
/// Adaptive n_probe widening due to epoch drift.
|
||||
NProbeWidened = 0x01,
|
||||
/// Adaptive n_probe widening due to degenerate distribution.
|
||||
DegenerateWidened = 0x02,
|
||||
/// Selective safety net scan on hot cache.
|
||||
SafetyNetSelective = 0x03,
|
||||
/// Safety net budget exhausted before completion.
|
||||
SafetyNetBudgetExhausted = 0x04,
|
||||
}
|
||||
|
||||
/// Structured reason for quality degradation.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum DegradationReason {
|
||||
/// Centroid epoch drift exceeded threshold.
|
||||
CentroidDrift { epoch_drift: u32, max_drift: u32 },
|
||||
/// Degenerate distance distribution detected.
|
||||
DegenerateDistribution { cv: f32, threshold: f32 },
|
||||
/// Budget exhausted during safety net scan.
|
||||
BudgetExhausted {
|
||||
scanned: u64,
|
||||
total: u64,
|
||||
budget_type: BudgetType,
|
||||
},
|
||||
/// Index layer not yet loaded.
|
||||
IndexNotLoaded { available: IndexLayersUsed },
|
||||
}
|
||||
|
||||
/// Which budget cap was hit.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum BudgetType {
|
||||
Time = 0x00,
|
||||
Candidates = 0x01,
|
||||
DistanceOps = 0x02,
|
||||
}
|
||||
|
||||
/// Why quality is degraded — full diagnostic report.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct DegradationReport {
|
||||
/// Which fallback path was chosen.
|
||||
pub fallback_path: FallbackPath,
|
||||
/// Why it was chosen (structured, not prose).
|
||||
pub reason: DegradationReason,
|
||||
/// What guarantee is lost relative to Full quality.
|
||||
pub guarantee_lost: &'static str,
|
||||
}
|
||||
|
||||
/// Budget caps for the brute-force safety net.
|
||||
///
|
||||
/// All three are enforced simultaneously. The scan stops at whichever hits
|
||||
/// first. These are runtime limits, not caller-adjustable above the defaults
|
||||
/// (unless `QualityPreference::PreferQuality`, which extends to 4x).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SafetyNetBudget {
|
||||
/// Maximum wall-clock time for the safety net scan (microseconds).
|
||||
pub max_scan_time_us: u64,
|
||||
/// Maximum number of candidate vectors to scan.
|
||||
pub max_scan_candidates: u64,
|
||||
/// Maximum number of distance evaluations.
|
||||
pub max_distance_ops: u64,
|
||||
}
|
||||
|
||||
impl SafetyNetBudget {
|
||||
/// Layer A only defaults: tight budget for instant first query.
|
||||
pub const LAYER_A: Self = Self {
|
||||
max_scan_time_us: 2_000, // 2 ms
|
||||
max_scan_candidates: 10_000,
|
||||
max_distance_ops: 10_000,
|
||||
};
|
||||
|
||||
/// Partial index defaults: moderate budget.
|
||||
pub const PARTIAL: Self = Self {
|
||||
max_scan_time_us: 5_000, // 5 ms
|
||||
max_scan_candidates: 50_000,
|
||||
max_distance_ops: 50_000,
|
||||
};
|
||||
|
||||
/// Full index: generous budget.
|
||||
pub const FULL: Self = Self {
|
||||
max_scan_time_us: 10_000, // 10 ms
|
||||
max_scan_candidates: 100_000,
|
||||
max_distance_ops: 100_000,
|
||||
};
|
||||
|
||||
/// Disabled: all zeros. Safety net will not scan anything.
|
||||
pub const DISABLED: Self = Self {
|
||||
max_scan_time_us: 0,
|
||||
max_scan_candidates: 0,
|
||||
max_distance_ops: 0,
|
||||
};
|
||||
|
||||
/// Extend all budgets by 4x for PreferQuality mode.
|
||||
/// Uses saturating arithmetic to prevent overflow.
|
||||
pub const fn extended_4x(&self) -> Self {
|
||||
Self {
|
||||
max_scan_time_us: self.max_scan_time_us.saturating_mul(4),
|
||||
max_scan_candidates: self.max_scan_candidates.saturating_mul(4),
|
||||
max_distance_ops: self.max_distance_ops.saturating_mul(4),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if all budgets are zero (disabled).
|
||||
pub const fn is_disabled(&self) -> bool {
|
||||
self.max_scan_time_us == 0 && self.max_scan_candidates == 0 && self.max_distance_ops == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SafetyNetBudget {
|
||||
fn default() -> Self {
|
||||
Self::LAYER_A
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive `ResponseQuality` from the worst `RetrievalQuality` in the result set.
|
||||
///
|
||||
/// Empty input returns `Unreliable` — zero results means zero confidence.
|
||||
pub fn derive_response_quality(retrieval_qualities: &[RetrievalQuality]) -> ResponseQuality {
|
||||
if retrieval_qualities.is_empty() {
|
||||
return ResponseQuality::Unreliable;
|
||||
}
|
||||
|
||||
let worst = retrieval_qualities
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by_key(|q| *q as u8)
|
||||
.unwrap_or(RetrievalQuality::Full);
|
||||
|
||||
match worst {
|
||||
RetrievalQuality::Full => ResponseQuality::Verified,
|
||||
RetrievalQuality::Partial => ResponseQuality::Usable,
|
||||
RetrievalQuality::LayerAOnly => ResponseQuality::Usable,
|
||||
RetrievalQuality::DegenerateDetected => ResponseQuality::Degraded,
|
||||
RetrievalQuality::BruteForceBudgeted => ResponseQuality::Degraded,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn retrieval_quality_ordering() {
|
||||
assert!(RetrievalQuality::Full < RetrievalQuality::BruteForceBudgeted);
|
||||
assert!(RetrievalQuality::Partial < RetrievalQuality::DegenerateDetected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_quality_ordering() {
|
||||
assert!(ResponseQuality::Verified < ResponseQuality::Unreliable);
|
||||
assert!(ResponseQuality::Usable < ResponseQuality::Degraded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_quality_full() {
|
||||
let q = derive_response_quality(&[RetrievalQuality::Full, RetrievalQuality::Full]);
|
||||
assert_eq!(q, ResponseQuality::Verified);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_quality_mixed() {
|
||||
let q = derive_response_quality(&[
|
||||
RetrievalQuality::Full,
|
||||
RetrievalQuality::DegenerateDetected,
|
||||
]);
|
||||
assert_eq!(q, ResponseQuality::Degraded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_quality_empty_is_unreliable() {
|
||||
let q = derive_response_quality(&[]);
|
||||
assert_eq!(q, ResponseQuality::Unreliable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_quality_layer_a() {
|
||||
let q = derive_response_quality(&[RetrievalQuality::LayerAOnly]);
|
||||
assert_eq!(q, ResponseQuality::Usable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_quality_brute_force() {
|
||||
let q = derive_response_quality(&[RetrievalQuality::BruteForceBudgeted]);
|
||||
assert_eq!(q, ResponseQuality::Degraded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safety_net_budget_layer_a() {
|
||||
let b = SafetyNetBudget::LAYER_A;
|
||||
assert_eq!(b.max_scan_time_us, 2_000);
|
||||
assert_eq!(b.max_scan_candidates, 10_000);
|
||||
assert_eq!(b.max_distance_ops, 10_000);
|
||||
assert!(!b.is_disabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safety_net_budget_extended() {
|
||||
let b = SafetyNetBudget::LAYER_A.extended_4x();
|
||||
assert_eq!(b.max_scan_time_us, 8_000);
|
||||
assert_eq!(b.max_scan_candidates, 40_000);
|
||||
assert_eq!(b.max_distance_ops, 40_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safety_net_budget_disabled() {
|
||||
let b = SafetyNetBudget::DISABLED;
|
||||
assert!(b.is_disabled());
|
||||
assert_eq!(b.max_scan_time_us, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_preference_default_is_auto() {
|
||||
assert_eq!(QualityPreference::default(), QualityPreference::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_repr_values() {
|
||||
assert_eq!(RetrievalQuality::Full as u8, 0x00);
|
||||
assert_eq!(RetrievalQuality::BruteForceBudgeted as u8, 0x04);
|
||||
assert_eq!(ResponseQuality::Verified as u8, 0x00);
|
||||
assert_eq!(ResponseQuality::Unreliable as u8, 0x03);
|
||||
assert_eq!(QualityPreference::Auto as u8, 0x00);
|
||||
assert_eq!(QualityPreference::AcceptDegraded as u8, 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_path_repr() {
|
||||
assert_eq!(FallbackPath::None as u8, 0x00);
|
||||
assert_eq!(FallbackPath::SafetyNetBudgetExhausted as u8, 0x04);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budget_report_default_is_zero() {
|
||||
let r = BudgetReport::default();
|
||||
assert_eq!(r.total_us, 0);
|
||||
assert_eq!(r.distance_ops, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degradation_report_construction() {
|
||||
let report = DegradationReport {
|
||||
fallback_path: FallbackPath::SafetyNetBudgetExhausted,
|
||||
reason: DegradationReason::BudgetExhausted {
|
||||
scanned: 5000,
|
||||
total: 10000,
|
||||
budget_type: BudgetType::DistanceOps,
|
||||
},
|
||||
guarantee_lost: "recall may be below target",
|
||||
};
|
||||
assert_eq!(report.fallback_path, FallbackPath::SafetyNetBudgetExhausted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evidence_summary_default() {
|
||||
let e = SearchEvidenceSummary::default();
|
||||
assert!(!e.degenerate_detected);
|
||||
assert_eq!(e.n_probe_effective, 0);
|
||||
assert_eq!(e.centroid_distance_cv, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_layers_default_all_false() {
|
||||
let l = IndexLayersUsed::default();
|
||||
assert!(!l.layer_a);
|
||||
assert!(!l.layer_b);
|
||||
assert!(!l.layer_c);
|
||||
assert!(!l.hot_cache);
|
||||
}
|
||||
}
|
||||
49
crates/rvf/rvf-types/src/quant_type.rs
Normal file
49
crates/rvf/rvf-types/src/quant_type.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Quantization type discriminator for QUANT_SEG payloads.
|
||||
|
||||
/// Identifies the quantization method stored in a QUANT_SEG.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum QuantType {
|
||||
/// Scalar quantization (min-max per dimension).
|
||||
Scalar = 0,
|
||||
/// Product quantization (codebook per subspace).
|
||||
Product = 1,
|
||||
/// Binary threshold quantization (sign bit per dimension).
|
||||
BinaryThreshold = 2,
|
||||
/// Residual product quantization.
|
||||
ResidualPq = 3,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for QuantType {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Scalar),
|
||||
1 => Ok(Self::Product),
|
||||
2 => Ok(Self::BinaryThreshold),
|
||||
3 => Ok(Self::ResidualPq),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for raw in 0..=3u8 {
|
||||
let qt = QuantType::try_from(raw).unwrap();
|
||||
assert_eq!(qt as u8, raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_value() {
|
||||
assert_eq!(QuantType::try_from(4), Err(4));
|
||||
assert_eq!(QuantType::try_from(255), Err(255));
|
||||
}
|
||||
}
|
||||
192
crates/rvf/rvf-types/src/refcount.rs
Normal file
192
crates/rvf/rvf-types/src/refcount.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
//! REFCOUNT_SEG (0x21) types for the RVF computational container.
|
||||
//!
|
||||
//! Defines the 32-byte `RefcountHeader` per ADR-031.
|
||||
//! The REFCOUNT_SEG tracks reference counts for shared clusters,
|
||||
//! enabling safe snapshot deletion and garbage collection.
|
||||
|
||||
use crate::error::RvfError;
|
||||
|
||||
/// Magic number for `RefcountHeader`: "RVRC" in big-endian.
|
||||
pub const REFCOUNT_MAGIC: u32 = 0x5256_5243;
|
||||
|
||||
/// 32-byte header for REFCOUNT_SEG payloads.
|
||||
///
|
||||
/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
|
||||
/// little-endian on the wire.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct RefcountHeader {
|
||||
/// Magic: `REFCOUNT_MAGIC` (0x52565243, "RVRC").
|
||||
pub magic: u32,
|
||||
/// RefcountHeader format version (currently 1).
|
||||
pub version: u16,
|
||||
/// Width of each refcount entry in bytes (1, 2, or 4).
|
||||
pub refcount_width: u8,
|
||||
/// Padding (must be zero).
|
||||
pub _pad: u8,
|
||||
/// Number of clusters tracked.
|
||||
pub cluster_count: u32,
|
||||
/// Maximum refcount value before overflow.
|
||||
pub max_refcount: u32,
|
||||
/// Offset to the refcount array within the segment payload.
|
||||
pub array_offset: u64,
|
||||
/// Snapshot epoch: 0 = mutable, >0 = frozen at this epoch.
|
||||
pub snapshot_epoch: u32,
|
||||
/// Reserved (must be zero).
|
||||
pub _reserved: u32,
|
||||
}
|
||||
|
||||
// Compile-time assertion: RefcountHeader must be exactly 32 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<RefcountHeader>() == 32);
|
||||
|
||||
impl RefcountHeader {
|
||||
/// Serialize the header to a 32-byte little-endian array.
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
let mut buf = [0u8; 32];
|
||||
buf[0x00..0x04].copy_from_slice(&self.magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.version.to_le_bytes());
|
||||
buf[0x06] = self.refcount_width;
|
||||
buf[0x07] = self._pad;
|
||||
buf[0x08..0x0C].copy_from_slice(&self.cluster_count.to_le_bytes());
|
||||
buf[0x0C..0x10].copy_from_slice(&self.max_refcount.to_le_bytes());
|
||||
buf[0x10..0x18].copy_from_slice(&self.array_offset.to_le_bytes());
|
||||
buf[0x18..0x1C].copy_from_slice(&self.snapshot_epoch.to_le_bytes());
|
||||
buf[0x1C..0x20].copy_from_slice(&self._reserved.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize a `RefcountHeader` from a 32-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 32]) -> Result<Self, RvfError> {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != REFCOUNT_MAGIC {
|
||||
return Err(RvfError::BadMagic {
|
||||
expected: REFCOUNT_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
|
||||
let refcount_width = data[0x06];
|
||||
let pad = data[0x07];
|
||||
let reserved = u32::from_le_bytes([data[0x1C], data[0x1D], data[0x1E], data[0x1F]]);
|
||||
|
||||
// Validate refcount_width is 1, 2, or 4 as specified
|
||||
if refcount_width != 1 && refcount_width != 2 && refcount_width != 4 {
|
||||
return Err(RvfError::InvalidEnumValue {
|
||||
type_name: "RefcountHeader::refcount_width",
|
||||
value: refcount_width as u64,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate padding and reserved fields are zero (spec requirement)
|
||||
if pad != 0 {
|
||||
return Err(RvfError::InvalidEnumValue {
|
||||
type_name: "RefcountHeader::_pad",
|
||||
value: pad as u64,
|
||||
});
|
||||
}
|
||||
if reserved != 0 {
|
||||
return Err(RvfError::InvalidEnumValue {
|
||||
type_name: "RefcountHeader::_reserved",
|
||||
value: reserved as u64,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version: u16::from_le_bytes([data[0x04], data[0x05]]),
|
||||
refcount_width,
|
||||
_pad: pad,
|
||||
cluster_count: u32::from_le_bytes([data[0x08], data[0x09], data[0x0A], data[0x0B]]),
|
||||
max_refcount: u32::from_le_bytes([data[0x0C], data[0x0D], data[0x0E], data[0x0F]]),
|
||||
array_offset: u64::from_le_bytes([
|
||||
data[0x10], data[0x11], data[0x12], data[0x13], data[0x14], data[0x15], data[0x16],
|
||||
data[0x17],
|
||||
]),
|
||||
snapshot_epoch: u32::from_le_bytes([data[0x18], data[0x19], data[0x1A], data[0x1B]]),
|
||||
_reserved: reserved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_header() -> RefcountHeader {
|
||||
RefcountHeader {
|
||||
magic: REFCOUNT_MAGIC,
|
||||
version: 1,
|
||||
refcount_width: 2,
|
||||
_pad: 0,
|
||||
cluster_count: 1024,
|
||||
max_refcount: 65535,
|
||||
array_offset: 64,
|
||||
snapshot_epoch: 0,
|
||||
_reserved: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_32() {
|
||||
assert_eq!(core::mem::size_of::<RefcountHeader>(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
let bytes_be = REFCOUNT_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVRC");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_header();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = RefcountHeader::from_bytes(&bytes).expect("from_bytes should succeed");
|
||||
|
||||
assert_eq!(decoded.magic, REFCOUNT_MAGIC);
|
||||
assert_eq!(decoded.version, 1);
|
||||
assert_eq!(decoded.refcount_width, 2);
|
||||
assert_eq!(decoded._pad, 0);
|
||||
assert_eq!(decoded.cluster_count, 1024);
|
||||
assert_eq!(decoded.max_refcount, 65535);
|
||||
assert_eq!(decoded.array_offset, 64);
|
||||
assert_eq!(decoded.snapshot_epoch, 0);
|
||||
assert_eq!(decoded._reserved, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_magic_returns_error() {
|
||||
let mut bytes = sample_header().to_bytes();
|
||||
bytes[0] = 0x00; // corrupt magic
|
||||
let err = RefcountHeader::from_bytes(&bytes).unwrap_err();
|
||||
match err {
|
||||
RvfError::BadMagic { expected, .. } => assert_eq!(expected, REFCOUNT_MAGIC),
|
||||
other => panic!("expected BadMagic, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
let h = sample_header();
|
||||
let base = &h as *const _ as usize;
|
||||
|
||||
assert_eq!(&h.magic as *const _ as usize - base, 0x00);
|
||||
assert_eq!(&h.version as *const _ as usize - base, 0x04);
|
||||
assert_eq!(&h.refcount_width as *const _ as usize - base, 0x06);
|
||||
assert_eq!(&h._pad as *const _ as usize - base, 0x07);
|
||||
assert_eq!(&h.cluster_count as *const _ as usize - base, 0x08);
|
||||
assert_eq!(&h.max_refcount as *const _ as usize - base, 0x0C);
|
||||
assert_eq!(&h.array_offset as *const _ as usize - base, 0x10);
|
||||
assert_eq!(&h.snapshot_epoch as *const _ as usize - base, 0x18);
|
||||
assert_eq!(&h._reserved as *const _ as usize - base, 0x1C);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_snapshot_epoch() {
|
||||
let mut h = sample_header();
|
||||
h.snapshot_epoch = 42;
|
||||
let bytes = h.to_bytes();
|
||||
let decoded = RefcountHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.snapshot_epoch, 42);
|
||||
}
|
||||
}
|
||||
408
crates/rvf/rvf-types/src/security.rs
Normal file
408
crates/rvf/rvf-types/src/security.rs
Normal file
@@ -0,0 +1,408 @@
|
||||
//! Security policy and error types for ADR-033 mandatory manifest signatures.
|
||||
//!
|
||||
//! Defines the `SecurityPolicy` mount policy (default: Strict) and
|
||||
//! structured `SecurityError` diagnostics for deterministic failure reasons.
|
||||
|
||||
/// Manifest signature verification policy.
|
||||
///
|
||||
/// Controls how the runtime handles unsigned or invalid signatures
|
||||
/// when opening an RVF file. Default is `Strict` — no signature means
|
||||
/// no mount in production.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum SecurityPolicy {
|
||||
/// No signature verification. For development and testing only.
|
||||
Permissive = 0x00,
|
||||
/// Warn on missing or invalid signatures, but allow open.
|
||||
WarnOnly = 0x01,
|
||||
/// Require valid signature on Level 0 manifest.
|
||||
/// DEFAULT for production.
|
||||
Strict = 0x02,
|
||||
/// Require valid signatures on Level 0, Level 1, and all
|
||||
/// hotset-referenced segments. Full chain verification.
|
||||
Paranoid = 0x03,
|
||||
}
|
||||
|
||||
impl Default for SecurityPolicy {
|
||||
fn default() -> Self {
|
||||
Self::Strict
|
||||
}
|
||||
}
|
||||
|
||||
impl SecurityPolicy {
|
||||
/// Returns true if signature verification is required at mount time.
|
||||
pub const fn requires_signature(&self) -> bool {
|
||||
matches!(*self, Self::Strict | Self::Paranoid)
|
||||
}
|
||||
|
||||
/// Returns true if content hash verification is performed on hotset access.
|
||||
pub const fn verifies_content_hashes(&self) -> bool {
|
||||
matches!(*self, Self::WarnOnly | Self::Strict | Self::Paranoid)
|
||||
}
|
||||
|
||||
/// Returns true if Level 1 manifest is also signature-verified.
|
||||
pub const fn verifies_level1(&self) -> bool {
|
||||
matches!(*self, Self::Paranoid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Structured security error with deterministic, stable error codes.
|
||||
///
|
||||
/// Every variant includes enough context for logging and diagnostics
|
||||
/// without exposing internal state that could aid an attacker.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum SecurityError {
|
||||
/// Level 0 manifest has no signature (sig_algo = 0).
|
||||
UnsignedManifest {
|
||||
/// Byte offset of the rejected manifest.
|
||||
manifest_offset: u64,
|
||||
},
|
||||
|
||||
/// Signature is present but cryptographically invalid.
|
||||
InvalidSignature {
|
||||
/// Byte offset of the rejected manifest.
|
||||
manifest_offset: u64,
|
||||
/// Phase where rejection occurred.
|
||||
rejection_phase: &'static str,
|
||||
},
|
||||
|
||||
/// Signature is valid but from an unknown/untrusted signer.
|
||||
UnknownSigner {
|
||||
/// Byte offset of the rejected manifest.
|
||||
manifest_offset: u64,
|
||||
/// Fingerprint of the actual signer (first 16 bytes of public key hash).
|
||||
actual_signer: [u8; 16],
|
||||
/// Fingerprint of the expected signer from trust store (if known).
|
||||
expected_signer: Option<[u8; 16]>,
|
||||
},
|
||||
|
||||
/// Content hash of a hotset-referenced segment does not match.
|
||||
ContentHashMismatch {
|
||||
/// Name of the pointer that failed (e.g., "centroid_seg_offset").
|
||||
pointer_name: &'static str,
|
||||
/// Content hash stored in Level 0.
|
||||
expected_hash: [u8; 16],
|
||||
/// Actual hash of the segment at the pointed offset.
|
||||
actual_hash: [u8; 16],
|
||||
/// Byte offset that was followed.
|
||||
seg_offset: u64,
|
||||
},
|
||||
|
||||
/// Centroid epoch drift exceeds maximum allowed.
|
||||
EpochDriftExceeded {
|
||||
/// Current epoch drift value.
|
||||
epoch_drift: u32,
|
||||
/// Maximum allowed drift.
|
||||
max_epoch_drift: u32,
|
||||
},
|
||||
|
||||
/// Level 1 manifest signature invalid (Paranoid mode only).
|
||||
Level1InvalidSignature {
|
||||
/// Byte offset of the Level 1 manifest.
|
||||
manifest_offset: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SecurityError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::UnsignedManifest { manifest_offset } => {
|
||||
write!(f, "unsigned manifest at offset 0x{manifest_offset:X}")
|
||||
}
|
||||
Self::InvalidSignature {
|
||||
manifest_offset,
|
||||
rejection_phase,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"invalid signature at offset 0x{manifest_offset:X} \
|
||||
(phase: {rejection_phase})"
|
||||
)
|
||||
}
|
||||
Self::UnknownSigner {
|
||||
manifest_offset, ..
|
||||
} => {
|
||||
write!(f, "unknown signer at offset 0x{manifest_offset:X}")
|
||||
}
|
||||
Self::ContentHashMismatch {
|
||||
pointer_name,
|
||||
seg_offset,
|
||||
..
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"content hash mismatch for {pointer_name} \
|
||||
at offset 0x{seg_offset:X}"
|
||||
)
|
||||
}
|
||||
Self::EpochDriftExceeded {
|
||||
epoch_drift,
|
||||
max_epoch_drift,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"centroid epoch drift {epoch_drift} exceeds max {max_epoch_drift}"
|
||||
)
|
||||
}
|
||||
Self::Level1InvalidSignature { manifest_offset } => {
|
||||
write!(
|
||||
f,
|
||||
"Level 1 manifest invalid signature at offset 0x{manifest_offset:X}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Content hash fields stored in the Level 0 reserved area (ADR-033 §1).
|
||||
///
|
||||
/// 96 bytes total: 5 content hashes (16 bytes each) + centroid_epoch (4) +
|
||||
/// max_epoch_drift (4) + reserved (8).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct HardeningFields {
|
||||
/// SHAKE-256 truncated to 128 bits of the entrypoint segment payload.
|
||||
pub entrypoint_content_hash: [u8; 16],
|
||||
/// SHAKE-256 truncated to 128 bits of the toplayer segment payload.
|
||||
pub toplayer_content_hash: [u8; 16],
|
||||
/// SHAKE-256 truncated to 128 bits of the centroid segment payload.
|
||||
pub centroid_content_hash: [u8; 16],
|
||||
/// SHAKE-256 truncated to 128 bits of the quantdict segment payload.
|
||||
pub quantdict_content_hash: [u8; 16],
|
||||
/// SHAKE-256 truncated to 128 bits of the hot_cache segment payload.
|
||||
pub hot_cache_content_hash: [u8; 16],
|
||||
/// Monotonic counter incremented on centroid recomputation.
|
||||
pub centroid_epoch: u32,
|
||||
/// Maximum allowed drift before forced recompute.
|
||||
pub max_epoch_drift: u32,
|
||||
/// Reserved for future hardening fields.
|
||||
pub reserved: [u8; 8],
|
||||
}
|
||||
|
||||
const _: () = assert!(core::mem::size_of::<HardeningFields>() == 96);
|
||||
|
||||
impl HardeningFields {
|
||||
/// Offset within the Level 0 reserved area (0xF00 + 109 = 0xF6D).
|
||||
/// Starts after FileIdentity (68 bytes), COW pointers (24 bytes),
|
||||
/// and double-root mechanism (17 bytes).
|
||||
pub const RESERVED_OFFSET: usize = 109;
|
||||
|
||||
/// Create zeroed hardening fields.
|
||||
pub const fn zeroed() -> Self {
|
||||
Self {
|
||||
entrypoint_content_hash: [0u8; 16],
|
||||
toplayer_content_hash: [0u8; 16],
|
||||
centroid_content_hash: [0u8; 16],
|
||||
quantdict_content_hash: [0u8; 16],
|
||||
hot_cache_content_hash: [0u8; 16],
|
||||
centroid_epoch: 0,
|
||||
max_epoch_drift: 64,
|
||||
reserved: [0u8; 8],
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize to 96 bytes (little-endian).
|
||||
pub fn to_bytes(&self) -> [u8; 96] {
|
||||
let mut buf = [0u8; 96];
|
||||
buf[0..16].copy_from_slice(&self.entrypoint_content_hash);
|
||||
buf[16..32].copy_from_slice(&self.toplayer_content_hash);
|
||||
buf[32..48].copy_from_slice(&self.centroid_content_hash);
|
||||
buf[48..64].copy_from_slice(&self.quantdict_content_hash);
|
||||
buf[64..80].copy_from_slice(&self.hot_cache_content_hash);
|
||||
buf[80..84].copy_from_slice(&self.centroid_epoch.to_le_bytes());
|
||||
buf[84..88].copy_from_slice(&self.max_epoch_drift.to_le_bytes());
|
||||
buf[88..96].copy_from_slice(&self.reserved);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize from 96 bytes (little-endian).
|
||||
pub fn from_bytes(buf: &[u8; 96]) -> Self {
|
||||
let mut entrypoint_content_hash = [0u8; 16];
|
||||
let mut toplayer_content_hash = [0u8; 16];
|
||||
let mut centroid_content_hash = [0u8; 16];
|
||||
let mut quantdict_content_hash = [0u8; 16];
|
||||
let mut hot_cache_content_hash = [0u8; 16];
|
||||
let mut reserved = [0u8; 8];
|
||||
|
||||
entrypoint_content_hash.copy_from_slice(&buf[0..16]);
|
||||
toplayer_content_hash.copy_from_slice(&buf[16..32]);
|
||||
centroid_content_hash.copy_from_slice(&buf[32..48]);
|
||||
quantdict_content_hash.copy_from_slice(&buf[48..64]);
|
||||
hot_cache_content_hash.copy_from_slice(&buf[64..80]);
|
||||
|
||||
let centroid_epoch = u32::from_le_bytes([buf[80], buf[81], buf[82], buf[83]]);
|
||||
let max_epoch_drift = u32::from_le_bytes([buf[84], buf[85], buf[86], buf[87]]);
|
||||
reserved.copy_from_slice(&buf[88..96]);
|
||||
|
||||
Self {
|
||||
entrypoint_content_hash,
|
||||
toplayer_content_hash,
|
||||
centroid_content_hash,
|
||||
quantdict_content_hash,
|
||||
hot_cache_content_hash,
|
||||
centroid_epoch,
|
||||
max_epoch_drift,
|
||||
reserved,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if all content hashes are zero (no hardening data stored).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entrypoint_content_hash == [0u8; 16]
|
||||
&& self.toplayer_content_hash == [0u8; 16]
|
||||
&& self.centroid_content_hash == [0u8; 16]
|
||||
&& self.quantdict_content_hash == [0u8; 16]
|
||||
&& self.hot_cache_content_hash == [0u8; 16]
|
||||
&& self.centroid_epoch == 0
|
||||
}
|
||||
|
||||
/// Get the content hash for a named pointer.
|
||||
pub fn hash_for_pointer(&self, pointer_name: &str) -> Option<&[u8; 16]> {
|
||||
match pointer_name {
|
||||
"entrypoint" => Some(&self.entrypoint_content_hash),
|
||||
"toplayer" => Some(&self.toplayer_content_hash),
|
||||
"centroid" => Some(&self.centroid_content_hash),
|
||||
"quantdict" => Some(&self.quantdict_content_hash),
|
||||
"hot_cache" => Some(&self.hot_cache_content_hash),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute epoch drift relative to the manifest's global epoch.
|
||||
pub fn epoch_drift(&self, manifest_epoch: u32) -> u32 {
|
||||
manifest_epoch.saturating_sub(self.centroid_epoch)
|
||||
}
|
||||
|
||||
/// Check if epoch drift exceeds the maximum allowed.
|
||||
pub fn is_epoch_drift_exceeded(&self, manifest_epoch: u32) -> bool {
|
||||
self.epoch_drift(manifest_epoch) > self.max_epoch_drift
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn security_policy_default_is_strict() {
|
||||
assert_eq!(SecurityPolicy::default(), SecurityPolicy::Strict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_policy_signature_required() {
|
||||
assert!(!SecurityPolicy::Permissive.requires_signature());
|
||||
assert!(!SecurityPolicy::WarnOnly.requires_signature());
|
||||
assert!(SecurityPolicy::Strict.requires_signature());
|
||||
assert!(SecurityPolicy::Paranoid.requires_signature());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_policy_content_hashes() {
|
||||
assert!(!SecurityPolicy::Permissive.verifies_content_hashes());
|
||||
assert!(SecurityPolicy::WarnOnly.verifies_content_hashes());
|
||||
assert!(SecurityPolicy::Strict.verifies_content_hashes());
|
||||
assert!(SecurityPolicy::Paranoid.verifies_content_hashes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_policy_level1() {
|
||||
assert!(!SecurityPolicy::Strict.verifies_level1());
|
||||
assert!(SecurityPolicy::Paranoid.verifies_level1());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_policy_repr() {
|
||||
assert_eq!(SecurityPolicy::Permissive as u8, 0x00);
|
||||
assert_eq!(SecurityPolicy::WarnOnly as u8, 0x01);
|
||||
assert_eq!(SecurityPolicy::Strict as u8, 0x02);
|
||||
assert_eq!(SecurityPolicy::Paranoid as u8, 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardening_fields_size() {
|
||||
assert_eq!(core::mem::size_of::<HardeningFields>(), 96);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardening_fields_round_trip() {
|
||||
let fields = HardeningFields {
|
||||
entrypoint_content_hash: [1u8; 16],
|
||||
toplayer_content_hash: [2u8; 16],
|
||||
centroid_content_hash: [3u8; 16],
|
||||
quantdict_content_hash: [4u8; 16],
|
||||
hot_cache_content_hash: [5u8; 16],
|
||||
centroid_epoch: 42,
|
||||
max_epoch_drift: 64,
|
||||
reserved: [0u8; 8],
|
||||
};
|
||||
let bytes = fields.to_bytes();
|
||||
let decoded = HardeningFields::from_bytes(&bytes);
|
||||
assert_eq!(fields, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardening_fields_zeroed() {
|
||||
let fields = HardeningFields::zeroed();
|
||||
assert!(fields.is_empty());
|
||||
assert_eq!(fields.max_epoch_drift, 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardening_fields_hash_for_pointer() {
|
||||
let mut fields = HardeningFields::zeroed();
|
||||
fields.centroid_content_hash = [0xAB; 16];
|
||||
assert_eq!(fields.hash_for_pointer("centroid"), Some(&[0xAB; 16]));
|
||||
assert_eq!(fields.hash_for_pointer("unknown"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardening_fields_epoch_drift() {
|
||||
let fields = HardeningFields {
|
||||
centroid_epoch: 10,
|
||||
max_epoch_drift: 64,
|
||||
..HardeningFields::zeroed()
|
||||
};
|
||||
assert_eq!(fields.epoch_drift(50), 40);
|
||||
assert!(!fields.is_epoch_drift_exceeded(50));
|
||||
assert!(fields.is_epoch_drift_exceeded(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_error_display() {
|
||||
let err = SecurityError::UnsignedManifest {
|
||||
manifest_offset: 0x1000,
|
||||
};
|
||||
let s = alloc::format!("{err}");
|
||||
assert!(s.contains("unsigned manifest"));
|
||||
|
||||
let err = SecurityError::ContentHashMismatch {
|
||||
pointer_name: "centroid",
|
||||
expected_hash: [0xAA; 16],
|
||||
actual_hash: [0xBB; 16],
|
||||
seg_offset: 0x2000,
|
||||
};
|
||||
let s = alloc::format!("{err}");
|
||||
assert!(s.contains("centroid"));
|
||||
assert!(s.contains("2000"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_error_unknown_signer() {
|
||||
let err = SecurityError::UnknownSigner {
|
||||
manifest_offset: 0x3000,
|
||||
actual_signer: [0x11; 16],
|
||||
expected_signer: Some([0x22; 16]),
|
||||
};
|
||||
let s = alloc::format!("{err}");
|
||||
assert!(s.contains("unknown signer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_offset_fits() {
|
||||
// 109 + 96 = 205 <= 252 (reserved area size)
|
||||
assert!(HardeningFields::RESERVED_OFFSET + 96 <= 252);
|
||||
}
|
||||
}
|
||||
132
crates/rvf/rvf-types/src/segment.rs
Normal file
132
crates/rvf/rvf-types/src/segment.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! 64-byte segment header for the RVF format.
|
||||
|
||||
/// The fixed 64-byte header that precedes every segment payload.
|
||||
///
|
||||
/// Layout matches the wire format exactly (repr(C), little-endian fields).
|
||||
/// Aligned to 64 bytes to match SIMD register width and cache-line size.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(C)]
|
||||
pub struct SegmentHeader {
|
||||
/// Magic number: must be `0x52564653` ("RVFS").
|
||||
pub magic: u32,
|
||||
/// Segment format version (currently 1).
|
||||
pub version: u8,
|
||||
/// Segment type discriminator (see `SegmentType`).
|
||||
pub seg_type: u8,
|
||||
/// Bitfield flags (see `SegmentFlags`).
|
||||
pub flags: u16,
|
||||
/// Monotonically increasing segment ordinal.
|
||||
pub segment_id: u64,
|
||||
/// Byte length of payload (after header, before optional footer).
|
||||
pub payload_length: u64,
|
||||
/// Nanosecond UNIX timestamp of segment creation.
|
||||
pub timestamp_ns: u64,
|
||||
/// Hash algorithm enum: 0=CRC32C, 1=XXH3-128, 2=SHAKE-256.
|
||||
pub checksum_algo: u8,
|
||||
/// Compression enum: 0=none, 1=LZ4, 2=ZSTD, 3=custom.
|
||||
pub compression: u8,
|
||||
/// Reserved (must be zero).
|
||||
pub reserved_0: u16,
|
||||
/// Reserved (must be zero).
|
||||
pub reserved_1: u32,
|
||||
/// First 128 bits of payload hash (algorithm per `checksum_algo`).
|
||||
pub content_hash: [u8; 16],
|
||||
/// Original payload size before compression (0 if uncompressed).
|
||||
pub uncompressed_len: u32,
|
||||
/// Padding to reach the 64-byte boundary.
|
||||
pub alignment_pad: u32,
|
||||
}
|
||||
|
||||
// Compile-time assertion: SegmentHeader must be exactly 64 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<SegmentHeader>() == 64);
|
||||
|
||||
impl SegmentHeader {
|
||||
/// Create a new segment header with the given type and segment ID.
|
||||
/// All other fields are set to defaults.
|
||||
pub const fn new(seg_type: u8, segment_id: u64) -> Self {
|
||||
Self {
|
||||
magic: crate::constants::SEGMENT_MAGIC,
|
||||
version: crate::constants::SEGMENT_VERSION,
|
||||
seg_type,
|
||||
flags: 0,
|
||||
segment_id,
|
||||
payload_length: 0,
|
||||
timestamp_ns: 0,
|
||||
checksum_algo: 0,
|
||||
compression: 0,
|
||||
reserved_0: 0,
|
||||
reserved_1: 0,
|
||||
content_hash: [0u8; 16],
|
||||
uncompressed_len: 0,
|
||||
alignment_pad: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the magic field matches the expected value.
|
||||
#[inline]
|
||||
pub const fn is_valid_magic(&self) -> bool {
|
||||
self.magic == crate::constants::SEGMENT_MAGIC
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::constants::SEGMENT_MAGIC;
|
||||
|
||||
#[test]
|
||||
fn header_size_is_64() {
|
||||
assert_eq!(core::mem::size_of::<SegmentHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_alignment() {
|
||||
assert!(core::mem::align_of::<SegmentHeader>() <= 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_header_has_valid_magic() {
|
||||
let h = SegmentHeader::new(0x01, 42);
|
||||
assert!(h.is_valid_magic());
|
||||
assert_eq!(h.magic, SEGMENT_MAGIC);
|
||||
assert_eq!(h.seg_type, 0x01);
|
||||
assert_eq!(h.segment_id, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_offsets() {
|
||||
// Verify field offsets match the wire format spec
|
||||
let h = SegmentHeader::new(0x01, 0);
|
||||
let base = &h as *const _ as usize;
|
||||
let magic_off = &h.magic as *const _ as usize - base;
|
||||
let version_off = &h.version as *const _ as usize - base;
|
||||
let seg_type_off = &h.seg_type as *const _ as usize - base;
|
||||
let flags_off = &h.flags as *const _ as usize - base;
|
||||
let segment_id_off = &h.segment_id as *const _ as usize - base;
|
||||
let payload_length_off = &h.payload_length as *const _ as usize - base;
|
||||
let timestamp_ns_off = &h.timestamp_ns as *const _ as usize - base;
|
||||
let checksum_algo_off = &h.checksum_algo as *const _ as usize - base;
|
||||
let compression_off = &h.compression as *const _ as usize - base;
|
||||
let reserved_0_off = &h.reserved_0 as *const _ as usize - base;
|
||||
let reserved_1_off = &h.reserved_1 as *const _ as usize - base;
|
||||
let content_hash_off = &h.content_hash as *const _ as usize - base;
|
||||
let uncompressed_len_off = &h.uncompressed_len as *const _ as usize - base;
|
||||
let alignment_pad_off = &h.alignment_pad as *const _ as usize - base;
|
||||
|
||||
assert_eq!(magic_off, 0x00);
|
||||
assert_eq!(version_off, 0x04);
|
||||
assert_eq!(seg_type_off, 0x05);
|
||||
assert_eq!(flags_off, 0x06);
|
||||
assert_eq!(segment_id_off, 0x08);
|
||||
assert_eq!(payload_length_off, 0x10);
|
||||
assert_eq!(timestamp_ns_off, 0x18);
|
||||
assert_eq!(checksum_algo_off, 0x20);
|
||||
assert_eq!(compression_off, 0x21);
|
||||
assert_eq!(reserved_0_off, 0x22);
|
||||
assert_eq!(reserved_1_off, 0x24);
|
||||
assert_eq!(content_hash_off, 0x28);
|
||||
assert_eq!(uncompressed_len_off, 0x38);
|
||||
assert_eq!(alignment_pad_off, 0x3C);
|
||||
}
|
||||
}
|
||||
197
crates/rvf/rvf-types/src/segment_type.rs
Normal file
197
crates/rvf/rvf-types/src/segment_type.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
//! Segment type discriminator for the RVF format.
|
||||
|
||||
/// Identifies the kind of data stored in a segment.
|
||||
///
|
||||
/// Values `0x00` and `0xF0..=0xFF` are reserved.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum SegmentType {
|
||||
/// Not a valid segment (uninitialized / zeroed region).
|
||||
Invalid = 0x00,
|
||||
/// Raw vector payloads (the actual embeddings).
|
||||
Vec = 0x01,
|
||||
/// HNSW adjacency lists, entry points, routing tables.
|
||||
Index = 0x02,
|
||||
/// Graph overlay deltas, partition updates, min-cut witnesses.
|
||||
Overlay = 0x03,
|
||||
/// Metadata mutations (label changes, deletions, moves).
|
||||
Journal = 0x04,
|
||||
/// Segment directory, hotset pointers, epoch state.
|
||||
Manifest = 0x05,
|
||||
/// Quantization dictionaries and codebooks.
|
||||
Quant = 0x06,
|
||||
/// Arbitrary key-value metadata (tags, provenance, lineage).
|
||||
Meta = 0x07,
|
||||
/// Temperature-promoted hot data (vectors + neighbors).
|
||||
Hot = 0x08,
|
||||
/// Access counter sketches for temperature decisions.
|
||||
Sketch = 0x09,
|
||||
/// Capability manifests, proof of computation, audit trails.
|
||||
Witness = 0x0A,
|
||||
/// Domain profile declarations (RVDNA, RVText, etc.).
|
||||
Profile = 0x0B,
|
||||
/// Key material, signature chains, certificate anchors.
|
||||
Crypto = 0x0C,
|
||||
/// Metadata inverted indexes for filtered search.
|
||||
MetaIdx = 0x0D,
|
||||
/// Embedded kernel / unikernel image for self-booting.
|
||||
Kernel = 0x0E,
|
||||
/// Embedded eBPF program for kernel fast path.
|
||||
Ebpf = 0x0F,
|
||||
/// Embedded WASM bytecode for self-bootstrapping execution.
|
||||
///
|
||||
/// A WASM_SEG contains either a WASM microkernel (the RVF query engine
|
||||
/// compiled to wasm32) or a minimal WASM interpreter that can execute
|
||||
/// the microkernel. When both are present the file becomes fully
|
||||
/// self-bootstrapping: any host with raw execution capability can run
|
||||
/// the embedded interpreter, which in turn runs the microkernel, which
|
||||
/// processes the RVF data segments.
|
||||
Wasm = 0x10,
|
||||
/// Embedded web dashboard bundle (HTML/JS/CSS assets).
|
||||
///
|
||||
/// A DASHBOARD_SEG contains a pre-built web application (e.g. Vite +
|
||||
/// Three.js) that can be served by the RVF HTTP server at `/`. The
|
||||
/// payload is a 64-byte `DashboardHeader` followed by a file table
|
||||
/// and concatenated file contents.
|
||||
Dashboard = 0x11,
|
||||
/// COW cluster mapping.
|
||||
CowMap = 0x20,
|
||||
/// Cluster reference counts.
|
||||
Refcount = 0x21,
|
||||
/// Vector membership filter.
|
||||
Membership = 0x22,
|
||||
/// Sparse delta patches.
|
||||
Delta = 0x23,
|
||||
/// Serialized transfer prior (cross-domain posterior summaries + cost EMAs).
|
||||
TransferPrior = 0x30,
|
||||
/// Policy kernel configuration and performance history.
|
||||
PolicyKernel = 0x31,
|
||||
/// Cost curve convergence data for acceleration tracking.
|
||||
CostCurve = 0x32,
|
||||
/// Federated learning export manifest: contributor pseudonym, export timestamp,
|
||||
/// included segment IDs, privacy budget spent, format version.
|
||||
FederatedManifest = 0x33,
|
||||
/// Differential privacy attestation: epsilon/delta values, noise mechanism,
|
||||
/// sensitivity bounds, clipping parameters.
|
||||
DiffPrivacyProof = 0x34,
|
||||
/// PII stripping attestation: redacted fields, rules fired,
|
||||
/// hash of pre-redaction content.
|
||||
RedactionLog = 0x35,
|
||||
/// Federated-averaged SONA weights: aggregated LoRA deltas,
|
||||
/// participation count, round number, convergence metrics.
|
||||
AggregateWeights = 0x36,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for SegmentType {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::Invalid),
|
||||
0x01 => Ok(Self::Vec),
|
||||
0x02 => Ok(Self::Index),
|
||||
0x03 => Ok(Self::Overlay),
|
||||
0x04 => Ok(Self::Journal),
|
||||
0x05 => Ok(Self::Manifest),
|
||||
0x06 => Ok(Self::Quant),
|
||||
0x07 => Ok(Self::Meta),
|
||||
0x08 => Ok(Self::Hot),
|
||||
0x09 => Ok(Self::Sketch),
|
||||
0x0A => Ok(Self::Witness),
|
||||
0x0B => Ok(Self::Profile),
|
||||
0x0C => Ok(Self::Crypto),
|
||||
0x0D => Ok(Self::MetaIdx),
|
||||
0x0E => Ok(Self::Kernel),
|
||||
0x0F => Ok(Self::Ebpf),
|
||||
0x10 => Ok(Self::Wasm),
|
||||
0x11 => Ok(Self::Dashboard),
|
||||
0x20 => Ok(Self::CowMap),
|
||||
0x21 => Ok(Self::Refcount),
|
||||
0x22 => Ok(Self::Membership),
|
||||
0x23 => Ok(Self::Delta),
|
||||
0x30 => Ok(Self::TransferPrior),
|
||||
0x31 => Ok(Self::PolicyKernel),
|
||||
0x32 => Ok(Self::CostCurve),
|
||||
0x33 => Ok(Self::FederatedManifest),
|
||||
0x34 => Ok(Self::DiffPrivacyProof),
|
||||
0x35 => Ok(Self::RedactionLog),
|
||||
0x36 => Ok(Self::AggregateWeights),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip_all_variants() {
|
||||
let variants = [
|
||||
SegmentType::Invalid,
|
||||
SegmentType::Vec,
|
||||
SegmentType::Index,
|
||||
SegmentType::Overlay,
|
||||
SegmentType::Journal,
|
||||
SegmentType::Manifest,
|
||||
SegmentType::Quant,
|
||||
SegmentType::Meta,
|
||||
SegmentType::Hot,
|
||||
SegmentType::Sketch,
|
||||
SegmentType::Witness,
|
||||
SegmentType::Profile,
|
||||
SegmentType::Crypto,
|
||||
SegmentType::MetaIdx,
|
||||
SegmentType::Kernel,
|
||||
SegmentType::Ebpf,
|
||||
SegmentType::Wasm,
|
||||
SegmentType::Dashboard,
|
||||
SegmentType::CowMap,
|
||||
SegmentType::Refcount,
|
||||
SegmentType::Membership,
|
||||
SegmentType::Delta,
|
||||
SegmentType::TransferPrior,
|
||||
SegmentType::PolicyKernel,
|
||||
SegmentType::CostCurve,
|
||||
SegmentType::FederatedManifest,
|
||||
SegmentType::DiffPrivacyProof,
|
||||
SegmentType::RedactionLog,
|
||||
SegmentType::AggregateWeights,
|
||||
];
|
||||
for v in variants {
|
||||
let raw = v as u8;
|
||||
assert_eq!(SegmentType::try_from(raw), Ok(v));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_value_returns_err() {
|
||||
assert_eq!(SegmentType::try_from(0x12), Err(0x12));
|
||||
assert_eq!(SegmentType::try_from(0x37), Err(0x37));
|
||||
assert_eq!(SegmentType::try_from(0xF0), Err(0xF0));
|
||||
assert_eq!(SegmentType::try_from(0xFF), Err(0xFF));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_expansion_discriminants() {
|
||||
assert_eq!(SegmentType::TransferPrior as u8, 0x30);
|
||||
assert_eq!(SegmentType::PolicyKernel as u8, 0x31);
|
||||
assert_eq!(SegmentType::CostCurve as u8, 0x32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn federation_discriminants() {
|
||||
assert_eq!(SegmentType::FederatedManifest as u8, 0x33);
|
||||
assert_eq!(SegmentType::DiffPrivacyProof as u8, 0x34);
|
||||
assert_eq!(SegmentType::RedactionLog as u8, 0x35);
|
||||
assert_eq!(SegmentType::AggregateWeights as u8, 0x36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_ebpf_wasm_discriminants() {
|
||||
assert_eq!(SegmentType::Kernel as u8, 0x0E);
|
||||
assert_eq!(SegmentType::Ebpf as u8, 0x0F);
|
||||
assert_eq!(SegmentType::Wasm as u8, 0x10);
|
||||
}
|
||||
}
|
||||
341
crates/rvf/rvf-types/src/sha256.rs
Normal file
341
crates/rvf/rvf-types/src/sha256.rs
Normal file
@@ -0,0 +1,341 @@
|
||||
//! Pure no_std SHA-256 (FIPS 180-4) and HMAC-SHA256 (RFC 2104).
|
||||
//!
|
||||
//! Zero external dependencies. Verified against NIST test vectors.
|
||||
|
||||
/// SHA-256 digest size in bytes.
|
||||
pub const DIGEST_SIZE: usize = 32;
|
||||
|
||||
/// SHA-256 block size in bytes.
|
||||
pub const BLOCK_SIZE: usize = 64;
|
||||
|
||||
/// Round constants: first 32 bits of fractional parts of cube roots of first 64 primes.
|
||||
const K: [u32; 64] = [
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||
];
|
||||
|
||||
/// Initial hash values: first 32 bits of fractional parts of square roots of first 8 primes.
|
||||
const H_INIT: [u32; 8] = [
|
||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
||||
];
|
||||
|
||||
/// Streaming SHA-256 hasher.
|
||||
pub struct Sha256 {
|
||||
state: [u32; 8],
|
||||
buffer: [u8; 64],
|
||||
buffer_len: usize,
|
||||
total_len: u64,
|
||||
}
|
||||
|
||||
impl Sha256 {
|
||||
/// Create a new SHA-256 hasher.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: H_INIT,
|
||||
buffer: [0u8; 64],
|
||||
buffer_len: 0,
|
||||
total_len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed data into the hasher.
|
||||
pub fn update(&mut self, data: &[u8]) {
|
||||
self.total_len += data.len() as u64;
|
||||
let mut offset = 0;
|
||||
|
||||
// Fill partial buffer.
|
||||
if self.buffer_len > 0 {
|
||||
let need = 64 - self.buffer_len;
|
||||
let take = if need < data.len() { need } else { data.len() };
|
||||
self.buffer[self.buffer_len..self.buffer_len + take].copy_from_slice(&data[..take]);
|
||||
self.buffer_len += take;
|
||||
offset = take;
|
||||
if self.buffer_len == 64 {
|
||||
let block = self.buffer;
|
||||
self.compress(&block);
|
||||
self.buffer_len = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Process full blocks directly.
|
||||
while offset + 64 <= data.len() {
|
||||
let mut block = [0u8; 64];
|
||||
block.copy_from_slice(&data[offset..offset + 64]);
|
||||
self.compress(&block);
|
||||
offset += 64;
|
||||
}
|
||||
|
||||
// Buffer remaining.
|
||||
let remaining = data.len() - offset;
|
||||
if remaining > 0 {
|
||||
self.buffer[..remaining].copy_from_slice(&data[offset..]);
|
||||
self.buffer_len = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
/// Finalize and return the 32-byte digest.
|
||||
pub fn finalize(mut self) -> [u8; 32] {
|
||||
let bit_len = self.total_len * 8;
|
||||
|
||||
// Append 0x80 padding byte.
|
||||
self.buffer[self.buffer_len] = 0x80;
|
||||
self.buffer_len += 1;
|
||||
|
||||
// If no room for 8-byte length, process block and start new one.
|
||||
if self.buffer_len > 56 {
|
||||
while self.buffer_len < 64 {
|
||||
self.buffer[self.buffer_len] = 0;
|
||||
self.buffer_len += 1;
|
||||
}
|
||||
let block = self.buffer;
|
||||
self.compress(&block);
|
||||
self.buffer = [0u8; 64];
|
||||
self.buffer_len = 0;
|
||||
}
|
||||
|
||||
// Zero-fill up to byte 56.
|
||||
while self.buffer_len < 56 {
|
||||
self.buffer[self.buffer_len] = 0;
|
||||
self.buffer_len += 1;
|
||||
}
|
||||
|
||||
// Append bit length as big-endian u64.
|
||||
self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
|
||||
let block = self.buffer;
|
||||
self.compress(&block);
|
||||
|
||||
// Produce output.
|
||||
let mut out = [0u8; 32];
|
||||
for i in 0..8 {
|
||||
out[i * 4..(i + 1) * 4].copy_from_slice(&self.state[i].to_be_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Process a single 64-byte block.
|
||||
fn compress(&mut self, block: &[u8; 64]) {
|
||||
let mut w = [0u32; 64];
|
||||
|
||||
// First 16 words from the block (big-endian).
|
||||
for i in 0..16 {
|
||||
w[i] = u32::from_be_bytes([
|
||||
block[i * 4],
|
||||
block[i * 4 + 1],
|
||||
block[i * 4 + 2],
|
||||
block[i * 4 + 3],
|
||||
]);
|
||||
}
|
||||
|
||||
// Extend to 64 words.
|
||||
for i in 16..64 {
|
||||
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
|
||||
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
|
||||
w[i] = w[i - 16]
|
||||
.wrapping_add(s0)
|
||||
.wrapping_add(w[i - 7])
|
||||
.wrapping_add(s1);
|
||||
}
|
||||
|
||||
// Initialize working variables.
|
||||
let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = self.state;
|
||||
|
||||
// 64 rounds.
|
||||
for i in 0..64 {
|
||||
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
|
||||
let ch = (e & f) ^ ((!e) & g);
|
||||
let temp1 = h
|
||||
.wrapping_add(s1)
|
||||
.wrapping_add(ch)
|
||||
.wrapping_add(K[i])
|
||||
.wrapping_add(w[i]);
|
||||
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
|
||||
let maj = (a & b) ^ (a & c) ^ (b & c);
|
||||
let temp2 = s0.wrapping_add(maj);
|
||||
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d.wrapping_add(temp1);
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = temp1.wrapping_add(temp2);
|
||||
}
|
||||
|
||||
// Update state.
|
||||
self.state[0] = self.state[0].wrapping_add(a);
|
||||
self.state[1] = self.state[1].wrapping_add(b);
|
||||
self.state[2] = self.state[2].wrapping_add(c);
|
||||
self.state[3] = self.state[3].wrapping_add(d);
|
||||
self.state[4] = self.state[4].wrapping_add(e);
|
||||
self.state[5] = self.state[5].wrapping_add(f);
|
||||
self.state[6] = self.state[6].wrapping_add(g);
|
||||
self.state[7] = self.state[7].wrapping_add(h);
|
||||
}
|
||||
}
|
||||
|
||||
/// One-shot SHA-256 hash.
|
||||
pub fn sha256(data: &[u8]) -> [u8; 32] {
|
||||
let mut h = Sha256::new();
|
||||
h.update(data);
|
||||
h.finalize()
|
||||
}
|
||||
|
||||
/// HMAC-SHA256 (RFC 2104).
|
||||
pub fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
|
||||
// If key > block size, hash it.
|
||||
let key_hash: [u8; 32];
|
||||
let k: &[u8] = if key.len() > BLOCK_SIZE {
|
||||
key_hash = sha256(key);
|
||||
&key_hash
|
||||
} else {
|
||||
key
|
||||
};
|
||||
|
||||
// Pad key to block size.
|
||||
let mut k_pad = [0u8; BLOCK_SIZE];
|
||||
k_pad[..k.len()].copy_from_slice(k);
|
||||
|
||||
// Inner hash: H((K ⊕ ipad) || message)
|
||||
let mut inner_key = [0u8; BLOCK_SIZE];
|
||||
for i in 0..BLOCK_SIZE {
|
||||
inner_key[i] = k_pad[i] ^ 0x36;
|
||||
}
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&inner_key);
|
||||
hasher.update(message);
|
||||
let inner_hash = hasher.finalize();
|
||||
|
||||
// Outer hash: H((K ⊕ opad) || inner_hash)
|
||||
let mut outer_key = [0u8; BLOCK_SIZE];
|
||||
for i in 0..BLOCK_SIZE {
|
||||
outer_key[i] = k_pad[i] ^ 0x5c;
|
||||
}
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&outer_key);
|
||||
hasher.update(&inner_hash);
|
||||
hasher.finalize()
|
||||
}
|
||||
|
||||
/// Constant-time comparison of two 32-byte digests.
|
||||
pub fn ct_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
|
||||
let mut diff = 0u8;
|
||||
for i in 0..32 {
|
||||
diff |= a[i] ^ b[i];
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Parse a hex string to a 32-byte array.
|
||||
fn hex32(hex: &str) -> [u8; 32] {
|
||||
let mut out = [0u8; 32];
|
||||
for i in 0..32 {
|
||||
out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// --- NIST SHA-256 Test Vectors (FIPS 180-4 examples) ---
|
||||
|
||||
#[test]
|
||||
fn sha256_empty() {
|
||||
let expected = hex32("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
assert_eq!(sha256(b""), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha256_abc() {
|
||||
let expected = hex32("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
assert_eq!(sha256(b"abc"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha256_two_block() {
|
||||
// "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" — 56 bytes, spans two blocks.
|
||||
let expected = hex32("248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
|
||||
assert_eq!(
|
||||
sha256(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha256_streaming_matches_oneshot() {
|
||||
let data = b"The quick brown fox jumps over the lazy dog";
|
||||
let expected = sha256(data);
|
||||
|
||||
// Feed in small chunks.
|
||||
let mut h = Sha256::new();
|
||||
h.update(&data[..10]);
|
||||
h.update(&data[10..30]);
|
||||
h.update(&data[30..]);
|
||||
assert_eq!(h.finalize(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha256_exactly_64_bytes() {
|
||||
let data = [0x42u8; 64]; // Exactly one block.
|
||||
let result = sha256(&data);
|
||||
// Just verify it produces a valid digest (no panic).
|
||||
assert_ne!(result, [0u8; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha256_128_bytes() {
|
||||
let data = [0xAB; 128]; // Exactly two blocks.
|
||||
let result = sha256(&data);
|
||||
assert_ne!(result, [0u8; 32]);
|
||||
}
|
||||
|
||||
// --- HMAC-SHA256 Test Vector (RFC 4231 Test Case 2) ---
|
||||
|
||||
#[test]
|
||||
fn hmac_sha256_rfc4231_case2() {
|
||||
// Key = "Jefe", Data = "what do ya want for nothing?"
|
||||
let key = b"Jefe";
|
||||
let data = b"what do ya want for nothing?";
|
||||
let expected = hex32("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843");
|
||||
assert_eq!(hmac_sha256(key, data), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_sha256_rfc4231_case1() {
|
||||
// Key = 20 bytes of 0x0b, Data = "Hi There"
|
||||
let key = [0x0bu8; 20];
|
||||
let data = b"Hi There";
|
||||
let expected = hex32("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7");
|
||||
assert_eq!(hmac_sha256(&key, data), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_sha256_long_key() {
|
||||
// Key longer than block size (131 bytes of 0xaa).
|
||||
let key = [0xAAu8; 131];
|
||||
let data = b"Test Using Larger Than Block-Size Key - Hash Key First";
|
||||
let expected = hex32("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54");
|
||||
assert_eq!(hmac_sha256(&key, data), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_same() {
|
||||
let a = sha256(b"test");
|
||||
assert!(ct_eq(&a, &a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_different() {
|
||||
let a = sha256(b"test1");
|
||||
let b = sha256(b"test2");
|
||||
assert!(!ct_eq(&a, &b));
|
||||
}
|
||||
}
|
||||
120
crates/rvf/rvf-types/src/signature.rs
Normal file
120
crates/rvf/rvf-types/src/signature.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
//! Signature algorithm identifiers and the signature footer struct.
|
||||
|
||||
/// Cryptographic signature algorithm.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u16)]
|
||||
pub enum SignatureAlgo {
|
||||
/// Ed25519 (64-byte signature, classical).
|
||||
Ed25519 = 0,
|
||||
/// ML-DSA-65 (3,309-byte signature, NIST Level 3 post-quantum).
|
||||
MlDsa65 = 1,
|
||||
/// SLH-DSA-128s (7,856-byte signature, NIST Level 1 post-quantum).
|
||||
SlhDsa128s = 2,
|
||||
}
|
||||
|
||||
impl SignatureAlgo {
|
||||
/// Expected signature byte length for this algorithm.
|
||||
pub const fn sig_length(self) -> u16 {
|
||||
match self {
|
||||
Self::Ed25519 => 64,
|
||||
Self::MlDsa65 => 3309,
|
||||
Self::SlhDsa128s => 7856,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this algorithm provides post-quantum security.
|
||||
pub const fn is_post_quantum(self) -> bool {
|
||||
match self {
|
||||
Self::Ed25519 => false,
|
||||
Self::MlDsa65 | Self::SlhDsa128s => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for SignatureAlgo {
|
||||
type Error = u16;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Ed25519),
|
||||
1 => Ok(Self::MlDsa65),
|
||||
2 => Ok(Self::SlhDsa128s),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The signature footer appended after a segment payload when the `SIGNED`
|
||||
/// flag is set.
|
||||
///
|
||||
/// Wire layout (variable-length on the wire, fixed-size in memory):
|
||||
/// ```text
|
||||
/// Offset Type Field
|
||||
/// 0x00 u16 sig_algo
|
||||
/// 0x02 u16 sig_length
|
||||
/// 0x04 [u8] signature (sig_length bytes)
|
||||
/// var u32 footer_length (total footer size for backward scan)
|
||||
/// ```
|
||||
///
|
||||
/// This struct uses a fixed-size buffer large enough for the largest
|
||||
/// supported algorithm (SLH-DSA-128s = 7,856 bytes).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SignatureFooter {
|
||||
/// Signature algorithm.
|
||||
pub sig_algo: u16,
|
||||
/// Byte length of the signature.
|
||||
pub sig_length: u16,
|
||||
/// Signature bytes (only the first `sig_length` bytes are meaningful).
|
||||
pub signature: [u8; Self::MAX_SIG_LEN],
|
||||
/// Total footer size (for backward scanning).
|
||||
pub footer_length: u32,
|
||||
}
|
||||
|
||||
impl SignatureFooter {
|
||||
/// Maximum signature length across all supported algorithms.
|
||||
pub const MAX_SIG_LEN: usize = 7856;
|
||||
|
||||
/// Compute the expected footer length from the signature length.
|
||||
/// Layout: 2 (sig_algo) + 2 (sig_length) + sig_length + 4 (footer_length).
|
||||
pub const fn compute_footer_length(sig_length: u16) -> u32 {
|
||||
2 + 2 + sig_length as u32 + 4
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn algo_round_trip() {
|
||||
for raw in 0..=2u16 {
|
||||
let a = SignatureAlgo::try_from(raw).unwrap();
|
||||
assert_eq!(a as u16, raw);
|
||||
}
|
||||
assert_eq!(SignatureAlgo::try_from(3), Err(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sig_lengths() {
|
||||
assert_eq!(SignatureAlgo::Ed25519.sig_length(), 64);
|
||||
assert_eq!(SignatureAlgo::MlDsa65.sig_length(), 3309);
|
||||
assert_eq!(SignatureAlgo::SlhDsa128s.sig_length(), 7856);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_quantum_flag() {
|
||||
assert!(!SignatureAlgo::Ed25519.is_post_quantum());
|
||||
assert!(SignatureAlgo::MlDsa65.is_post_quantum());
|
||||
assert!(SignatureAlgo::SlhDsa128s.is_post_quantum());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn footer_length_computation() {
|
||||
// Ed25519: 2 + 2 + 64 + 4 = 72
|
||||
assert_eq!(SignatureFooter::compute_footer_length(64), 72);
|
||||
// ML-DSA-65: 2 + 2 + 3309 + 4 = 3317
|
||||
assert_eq!(SignatureFooter::compute_footer_length(3309), 3317);
|
||||
}
|
||||
}
|
||||
402
crates/rvf/rvf-types/src/wasm_bootstrap.rs
Normal file
402
crates/rvf/rvf-types/src/wasm_bootstrap.rs
Normal file
@@ -0,0 +1,402 @@
|
||||
//! WASM_SEG (0x10) types for self-bootstrapping RVF files.
|
||||
//!
|
||||
//! Defines the 64-byte `WasmHeader` and associated enums.
|
||||
//! A WASM_SEG embeds WASM bytecode that enables an RVF file to carry its
|
||||
//! own execution runtime. When combined with the data segments (VEC_SEG,
|
||||
//! INDEX_SEG, etc.), this makes the file fully self-bootstrapping:
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌──────────────────────────────────────────────────────────┐
|
||||
//! │ .rvf file │
|
||||
//! │ │
|
||||
//! │ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
|
||||
//! │ │ WASM_SEG │ │ WASM_SEG │ │ VEC_SEG │ │
|
||||
//! │ │ role=Interp │ │ role=uKernel │ │ (data) │ │
|
||||
//! │ │ ~50 KB │ │ ~5.5 KB │ │ │ │
|
||||
//! │ └──────┬──────┘ └──────┬───────┘ └───────┬───────┘ │
|
||||
//! │ │ │ │ │
|
||||
//! │ │ executes │ processes │ │
|
||||
//! │ └───────────────►└──────────────────►│ │
|
||||
//! │ │
|
||||
//! │ Layer 0: Raw bytes │
|
||||
//! │ Layer 1: Embedded WASM interpreter (native bootstrap) │
|
||||
//! │ Layer 2: WASM microkernel (query engine) │
|
||||
//! │ Layer 3: RVF data (vectors, indexes, manifests) │
|
||||
//! └──────────────────────────────────────────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! The host only needs raw execution capability. RVF becomes
|
||||
//! self-bootstrapping — "runs anywhere compute exists."
|
||||
|
||||
use crate::error::RvfError;
|
||||
|
||||
/// Magic number for `WasmHeader`: "RVWM" in big-endian.
|
||||
pub const WASM_MAGIC: u32 = 0x5256_574D;
|
||||
|
||||
/// Role of the embedded WASM module within the bootstrap chain.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum WasmRole {
|
||||
/// RVF microkernel: the query/ingest engine compiled to WASM.
|
||||
/// This is the 5.5 KB Cognitum tile runtime with 14+ exports.
|
||||
Microkernel = 0x00,
|
||||
/// Minimal WASM interpreter: enables self-bootstrapping on hosts
|
||||
/// that lack a native WASM runtime. The interpreter runs the
|
||||
/// microkernel, which then processes RVF data.
|
||||
Interpreter = 0x01,
|
||||
/// Combined interpreter + microkernel in a single module.
|
||||
/// The interpreter is linked with the microkernel for zero-copy
|
||||
/// bootstrap on bare environments.
|
||||
Combined = 0x02,
|
||||
/// Domain-specific extension module (e.g., custom distance
|
||||
/// functions, codon decoder for RVDNA, token scorer for RVText).
|
||||
Extension = 0x03,
|
||||
/// Control plane module: store management, export, segment
|
||||
/// parsing, and file-level operations.
|
||||
ControlPlane = 0x04,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for WasmRole {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::Microkernel),
|
||||
0x01 => Ok(Self::Interpreter),
|
||||
0x02 => Ok(Self::Combined),
|
||||
0x03 => Ok(Self::Extension),
|
||||
0x04 => Ok(Self::ControlPlane),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "WasmRole",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Target platform hint for the WASM module.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum WasmTarget {
|
||||
/// Generic wasm32 (runs on any compliant runtime).
|
||||
Wasm32 = 0x00,
|
||||
/// WASI Preview 1 (requires WASI syscalls).
|
||||
WasiP1 = 0x01,
|
||||
/// WASI Preview 2 (component model).
|
||||
WasiP2 = 0x02,
|
||||
/// Browser-optimized (expects Web APIs via imports).
|
||||
Browser = 0x03,
|
||||
/// Bare-metal tile (no imports beyond host-tile protocol).
|
||||
BareTile = 0x04,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for WasmTarget {
|
||||
type Error = RvfError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(Self::Wasm32),
|
||||
0x01 => Ok(Self::WasiP1),
|
||||
0x02 => Ok(Self::WasiP2),
|
||||
0x03 => Ok(Self::Browser),
|
||||
0x04 => Ok(Self::BareTile),
|
||||
_ => Err(RvfError::InvalidEnumValue {
|
||||
type_name: "WasmTarget",
|
||||
value: value as u64,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WASM module feature requirements (bitfield).
|
||||
pub const WASM_FEAT_SIMD: u16 = 1 << 0;
|
||||
pub const WASM_FEAT_BULK_MEMORY: u16 = 1 << 1;
|
||||
pub const WASM_FEAT_MULTI_VALUE: u16 = 1 << 2;
|
||||
pub const WASM_FEAT_REFERENCE_TYPES: u16 = 1 << 3;
|
||||
pub const WASM_FEAT_THREADS: u16 = 1 << 4;
|
||||
pub const WASM_FEAT_TAIL_CALL: u16 = 1 << 5;
|
||||
pub const WASM_FEAT_GC: u16 = 1 << 6;
|
||||
pub const WASM_FEAT_EXCEPTION_HANDLING: u16 = 1 << 7;
|
||||
|
||||
/// 64-byte header for WASM_SEG payloads.
|
||||
///
|
||||
/// Follows the standard 64-byte `SegmentHeader`. The WASM bytecode
|
||||
/// follows immediately after this header within the segment payload.
|
||||
///
|
||||
/// For self-bootstrapping files, two WASM_SEGs are present:
|
||||
/// 1. `role = Interpreter` — a minimal WASM interpreter (~50 KB)
|
||||
/// 2. `role = Microkernel` — the RVF query engine (~5.5 KB)
|
||||
///
|
||||
/// The bootstrap sequence is:
|
||||
/// 1. Host reads file, finds WASM_SEG with `role = Interpreter`
|
||||
/// 2. Host loads interpreter bytecode into any available execution engine
|
||||
/// 3. Interpreter instantiates the microkernel WASM_SEG
|
||||
/// 4. Microkernel processes VEC_SEG, INDEX_SEG, etc.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct WasmHeader {
|
||||
/// Magic: `WASM_MAGIC` (0x5256574D, "RVWM").
|
||||
pub wasm_magic: u32,
|
||||
/// WasmHeader format version (currently 1).
|
||||
pub header_version: u16,
|
||||
/// Role in the bootstrap chain (see `WasmRole`).
|
||||
pub role: u8,
|
||||
/// Target platform (see `WasmTarget`).
|
||||
pub target: u8,
|
||||
/// Required WASM features bitfield (see `WASM_FEAT_*`).
|
||||
pub required_features: u16,
|
||||
/// Number of exports in the WASM module.
|
||||
pub export_count: u16,
|
||||
/// Uncompressed WASM bytecode size (bytes).
|
||||
pub bytecode_size: u32,
|
||||
/// Compressed bytecode size (0 if uncompressed).
|
||||
pub compressed_size: u32,
|
||||
/// Compression algorithm (same enum as SegmentHeader).
|
||||
pub compression: u8,
|
||||
/// Minimum linear memory pages required (64 KB each).
|
||||
pub min_memory_pages: u8,
|
||||
/// Maximum linear memory pages (0 = no limit).
|
||||
pub max_memory_pages: u8,
|
||||
/// Number of WASM tables.
|
||||
pub table_count: u8,
|
||||
/// SHAKE-256-256 hash of uncompressed bytecode.
|
||||
pub bytecode_hash: [u8; 32],
|
||||
/// Priority order for bootstrap resolution (lower = tried first).
|
||||
/// The interpreter with lowest priority is used when multiple are present.
|
||||
pub bootstrap_priority: u8,
|
||||
/// If role=Interpreter, this is the interpreter type:
|
||||
/// 0x00 = generic stack machine, 0x01 = wasm3-compatible,
|
||||
/// 0x02 = wamr-compatible, 0x03 = wasmi-compatible.
|
||||
pub interpreter_type: u8,
|
||||
/// Reserved (must be zero).
|
||||
pub reserved: [u8; 6],
|
||||
}
|
||||
|
||||
// Compile-time assertion: WasmHeader must be exactly 64 bytes.
|
||||
const _: () = assert!(core::mem::size_of::<WasmHeader>() == 64);
|
||||
|
||||
impl WasmHeader {
|
||||
/// Serialize the header to a 64-byte little-endian array.
|
||||
pub fn to_bytes(&self) -> [u8; 64] {
|
||||
let mut buf = [0u8; 64];
|
||||
buf[0x00..0x04].copy_from_slice(&self.wasm_magic.to_le_bytes());
|
||||
buf[0x04..0x06].copy_from_slice(&self.header_version.to_le_bytes());
|
||||
buf[0x06] = self.role;
|
||||
buf[0x07] = self.target;
|
||||
buf[0x08..0x0A].copy_from_slice(&self.required_features.to_le_bytes());
|
||||
buf[0x0A..0x0C].copy_from_slice(&self.export_count.to_le_bytes());
|
||||
buf[0x0C..0x10].copy_from_slice(&self.bytecode_size.to_le_bytes());
|
||||
buf[0x10..0x14].copy_from_slice(&self.compressed_size.to_le_bytes());
|
||||
buf[0x14] = self.compression;
|
||||
buf[0x15] = self.min_memory_pages;
|
||||
buf[0x16] = self.max_memory_pages;
|
||||
buf[0x17] = self.table_count;
|
||||
buf[0x18..0x38].copy_from_slice(&self.bytecode_hash);
|
||||
buf[0x38] = self.bootstrap_priority;
|
||||
buf[0x39] = self.interpreter_type;
|
||||
buf[0x3A..0x40].copy_from_slice(&self.reserved);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize a `WasmHeader` from a 64-byte slice.
|
||||
pub fn from_bytes(data: &[u8; 64]) -> Result<Self, RvfError> {
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != WASM_MAGIC {
|
||||
return Err(RvfError::BadMagic {
|
||||
expected: WASM_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
wasm_magic: magic,
|
||||
header_version: u16::from_le_bytes([data[0x04], data[0x05]]),
|
||||
role: data[0x06],
|
||||
target: data[0x07],
|
||||
required_features: u16::from_le_bytes([data[0x08], data[0x09]]),
|
||||
export_count: u16::from_le_bytes([data[0x0A], data[0x0B]]),
|
||||
bytecode_size: u32::from_le_bytes([data[0x0C], data[0x0D], data[0x0E], data[0x0F]]),
|
||||
compressed_size: u32::from_le_bytes([data[0x10], data[0x11], data[0x12], data[0x13]]),
|
||||
compression: data[0x14],
|
||||
min_memory_pages: data[0x15],
|
||||
max_memory_pages: data[0x16],
|
||||
table_count: data[0x17],
|
||||
bytecode_hash: {
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(&data[0x18..0x38]);
|
||||
h
|
||||
},
|
||||
bootstrap_priority: data[0x38],
|
||||
interpreter_type: data[0x39],
|
||||
reserved: {
|
||||
let mut r = [0u8; 6];
|
||||
r.copy_from_slice(&data[0x3A..0x40]);
|
||||
r
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_header() -> WasmHeader {
|
||||
WasmHeader {
|
||||
wasm_magic: WASM_MAGIC,
|
||||
header_version: 1,
|
||||
role: WasmRole::Microkernel as u8,
|
||||
target: WasmTarget::BareTile as u8,
|
||||
required_features: WASM_FEAT_SIMD | WASM_FEAT_BULK_MEMORY,
|
||||
export_count: 14,
|
||||
bytecode_size: 5500,
|
||||
compressed_size: 0,
|
||||
compression: 0,
|
||||
min_memory_pages: 2, // 128 KB
|
||||
max_memory_pages: 4, // 256 KB
|
||||
table_count: 0,
|
||||
bytecode_hash: [0xAB; 32],
|
||||
bootstrap_priority: 0,
|
||||
interpreter_type: 0,
|
||||
reserved: [0; 6],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_size_is_64() {
|
||||
assert_eq!(core::mem::size_of::<WasmHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_bytes_match_ascii() {
|
||||
let bytes_be = WASM_MAGIC.to_be_bytes();
|
||||
assert_eq!(&bytes_be, b"RVWM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serialization() {
|
||||
let original = sample_header();
|
||||
let bytes = original.to_bytes();
|
||||
let decoded = WasmHeader::from_bytes(&bytes).expect("from_bytes should succeed");
|
||||
|
||||
assert_eq!(decoded.wasm_magic, WASM_MAGIC);
|
||||
assert_eq!(decoded.header_version, 1);
|
||||
assert_eq!(decoded.role, WasmRole::Microkernel as u8);
|
||||
assert_eq!(decoded.target, WasmTarget::BareTile as u8);
|
||||
assert_eq!(
|
||||
decoded.required_features,
|
||||
WASM_FEAT_SIMD | WASM_FEAT_BULK_MEMORY
|
||||
);
|
||||
assert_eq!(decoded.export_count, 14);
|
||||
assert_eq!(decoded.bytecode_size, 5500);
|
||||
assert_eq!(decoded.compressed_size, 0);
|
||||
assert_eq!(decoded.compression, 0);
|
||||
assert_eq!(decoded.min_memory_pages, 2);
|
||||
assert_eq!(decoded.max_memory_pages, 4);
|
||||
assert_eq!(decoded.table_count, 0);
|
||||
assert_eq!(decoded.bytecode_hash, [0xAB; 32]);
|
||||
assert_eq!(decoded.bootstrap_priority, 0);
|
||||
assert_eq!(decoded.interpreter_type, 0);
|
||||
assert_eq!(decoded.reserved, [0; 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_magic_returns_error() {
|
||||
let mut bytes = sample_header().to_bytes();
|
||||
bytes[0] = 0x00;
|
||||
let err = WasmHeader::from_bytes(&bytes).unwrap_err();
|
||||
match err {
|
||||
RvfError::BadMagic { expected, .. } => assert_eq!(expected, WASM_MAGIC),
|
||||
other => panic!("expected BadMagic, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpreter_header() {
|
||||
let h = WasmHeader {
|
||||
wasm_magic: WASM_MAGIC,
|
||||
header_version: 1,
|
||||
role: WasmRole::Interpreter as u8,
|
||||
target: WasmTarget::Wasm32 as u8,
|
||||
required_features: 0,
|
||||
export_count: 3,
|
||||
bytecode_size: 51_200, // ~50 KB interpreter
|
||||
compressed_size: 22_000,
|
||||
compression: 2, // ZSTD
|
||||
min_memory_pages: 16, // 1 MB
|
||||
max_memory_pages: 64, // 4 MB
|
||||
table_count: 1,
|
||||
bytecode_hash: [0xCD; 32],
|
||||
bootstrap_priority: 0, // highest priority
|
||||
interpreter_type: 0x03, // wasmi-compatible
|
||||
reserved: [0; 6],
|
||||
};
|
||||
let bytes = h.to_bytes();
|
||||
let decoded = WasmHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.role, WasmRole::Interpreter as u8);
|
||||
assert_eq!(decoded.bytecode_size, 51_200);
|
||||
assert_eq!(decoded.interpreter_type, 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_bootstrap_header() {
|
||||
let h = WasmHeader {
|
||||
wasm_magic: WASM_MAGIC,
|
||||
header_version: 1,
|
||||
role: WasmRole::Combined as u8,
|
||||
target: WasmTarget::Wasm32 as u8,
|
||||
required_features: WASM_FEAT_SIMD,
|
||||
export_count: 17,
|
||||
bytecode_size: 56_700, // interpreter + microkernel
|
||||
compressed_size: 0,
|
||||
compression: 0,
|
||||
min_memory_pages: 16,
|
||||
max_memory_pages: 64,
|
||||
table_count: 1,
|
||||
bytecode_hash: [0xEF; 32],
|
||||
bootstrap_priority: 0,
|
||||
interpreter_type: 0,
|
||||
reserved: [0; 6],
|
||||
};
|
||||
let bytes = h.to_bytes();
|
||||
let decoded = WasmHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.role, WasmRole::Combined as u8);
|
||||
assert_eq!(decoded.export_count, 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wasm_role_try_from() {
|
||||
assert_eq!(WasmRole::try_from(0x00), Ok(WasmRole::Microkernel));
|
||||
assert_eq!(WasmRole::try_from(0x01), Ok(WasmRole::Interpreter));
|
||||
assert_eq!(WasmRole::try_from(0x02), Ok(WasmRole::Combined));
|
||||
assert_eq!(WasmRole::try_from(0x03), Ok(WasmRole::Extension));
|
||||
assert_eq!(WasmRole::try_from(0x04), Ok(WasmRole::ControlPlane));
|
||||
assert!(WasmRole::try_from(0x05).is_err());
|
||||
assert!(WasmRole::try_from(0xFF).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wasm_target_try_from() {
|
||||
assert_eq!(WasmTarget::try_from(0x00), Ok(WasmTarget::Wasm32));
|
||||
assert_eq!(WasmTarget::try_from(0x01), Ok(WasmTarget::WasiP1));
|
||||
assert_eq!(WasmTarget::try_from(0x02), Ok(WasmTarget::WasiP2));
|
||||
assert_eq!(WasmTarget::try_from(0x03), Ok(WasmTarget::Browser));
|
||||
assert_eq!(WasmTarget::try_from(0x04), Ok(WasmTarget::BareTile));
|
||||
assert!(WasmTarget::try_from(0x05).is_err());
|
||||
assert!(WasmTarget::try_from(0xFF).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_flags_bit_positions() {
|
||||
assert_eq!(WASM_FEAT_SIMD, 0x0001);
|
||||
assert_eq!(WASM_FEAT_BULK_MEMORY, 0x0002);
|
||||
assert_eq!(WASM_FEAT_MULTI_VALUE, 0x0004);
|
||||
assert_eq!(WASM_FEAT_REFERENCE_TYPES, 0x0008);
|
||||
assert_eq!(WASM_FEAT_THREADS, 0x0010);
|
||||
assert_eq!(WASM_FEAT_TAIL_CALL, 0x0020);
|
||||
assert_eq!(WASM_FEAT_GC, 0x0040);
|
||||
assert_eq!(WASM_FEAT_EXCEPTION_HANDLING, 0x0080);
|
||||
}
|
||||
}
|
||||
513
crates/rvf/rvf-types/src/witness.rs
Normal file
513
crates/rvf/rvf-types/src/witness.rs
Normal file
@@ -0,0 +1,513 @@
|
||||
//! Witness bundle types for ADR-035 capability reports.
|
||||
//!
|
||||
//! A witness bundle is a signed, self-contained evidence record of a task
|
||||
//! execution. It captures spec, plan, tool trace, diff, test log, cost,
|
||||
//! latency, and governance mode — everything needed for deterministic replay
|
||||
//! and audit.
|
||||
//!
|
||||
//! Wire format: 64-byte header + TLV sections + optional HMAC-SHA256 signature.
|
||||
|
||||
/// Magic bytes for witness bundle: "RVWW" (RuVector Witness).
|
||||
pub const WITNESS_MAGIC: u32 = 0x5257_5657;
|
||||
|
||||
/// Size of the witness header in bytes.
|
||||
pub const WITNESS_HEADER_SIZE: usize = 64;
|
||||
|
||||
// --- Flags ---
|
||||
|
||||
/// Witness bundle is signed (HMAC-SHA256).
|
||||
pub const WIT_SIGNED: u16 = 0x0001;
|
||||
/// Witness bundle has a spec section.
|
||||
pub const WIT_HAS_SPEC: u16 = 0x0002;
|
||||
/// Witness bundle has a plan section.
|
||||
pub const WIT_HAS_PLAN: u16 = 0x0004;
|
||||
/// Witness bundle has a tool trace section.
|
||||
pub const WIT_HAS_TRACE: u16 = 0x0008;
|
||||
/// Witness bundle has a diff section.
|
||||
pub const WIT_HAS_DIFF: u16 = 0x0010;
|
||||
/// Witness bundle has a test log section.
|
||||
pub const WIT_HAS_TEST_LOG: u16 = 0x0020;
|
||||
/// Witness bundle has a postmortem section.
|
||||
pub const WIT_HAS_POSTMORTEM: u16 = 0x0040;
|
||||
|
||||
// --- TLV tags ---
|
||||
|
||||
/// Tag: task spec / prompt text.
|
||||
pub const WIT_TAG_SPEC: u16 = 0x0001;
|
||||
/// Tag: plan graph (text or structured).
|
||||
pub const WIT_TAG_PLAN: u16 = 0x0002;
|
||||
/// Tag: tool call trace (array of ToolCallEntry).
|
||||
pub const WIT_TAG_TRACE: u16 = 0x0003;
|
||||
/// Tag: code diff (unified diff text).
|
||||
pub const WIT_TAG_DIFF: u16 = 0x0004;
|
||||
/// Tag: test output log.
|
||||
pub const WIT_TAG_TEST_LOG: u16 = 0x0005;
|
||||
/// Tag: postmortem / failure analysis.
|
||||
pub const WIT_TAG_POSTMORTEM: u16 = 0x0006;
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
/// Task execution outcome.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum TaskOutcome {
|
||||
/// Task completed with passing tests and merged diff.
|
||||
Solved = 0,
|
||||
/// Task attempted but tests fail or diff rejected.
|
||||
Failed = 1,
|
||||
/// Task skipped (precondition not met).
|
||||
Skipped = 2,
|
||||
/// Task errored (infrastructure or tool failure).
|
||||
Errored = 3,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for TaskOutcome {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Solved),
|
||||
1 => Ok(Self::Failed),
|
||||
2 => Ok(Self::Skipped),
|
||||
3 => Ok(Self::Errored),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Governance mode under which the task was executed.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum GovernanceMode {
|
||||
/// Read-only plus suggestions. No writes.
|
||||
Restricted = 0,
|
||||
/// Writes allowed with human confirmation gates.
|
||||
Approved = 1,
|
||||
/// Bounded authority with automatic rollback on violation.
|
||||
Autonomous = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for GovernanceMode {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Restricted),
|
||||
1 => Ok(Self::Approved),
|
||||
2 => Ok(Self::Autonomous),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy check result for a single tool call.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum PolicyCheck {
|
||||
/// Tool call allowed by policy.
|
||||
Allowed = 0,
|
||||
/// Tool call denied by policy.
|
||||
Denied = 1,
|
||||
/// Tool call required human confirmation.
|
||||
Confirmed = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for PolicyCheck {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Allowed),
|
||||
1 => Ok(Self::Denied),
|
||||
2 => Ok(Self::Confirmed),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire-format witness header (exactly 64 bytes, `repr(C)`).
|
||||
///
|
||||
/// ```text
|
||||
/// Offset Type Field
|
||||
/// 0x00 u32 magic (0x52575657 "RVWW")
|
||||
/// 0x04 u16 version
|
||||
/// 0x06 u16 flags
|
||||
/// 0x08 [u8; 16] task_id (UUID)
|
||||
/// 0x18 [u8; 8] policy_hash (SHA-256 truncated)
|
||||
/// 0x20 u64 created_ns (UNIX epoch nanoseconds)
|
||||
/// 0x28 u8 outcome (TaskOutcome)
|
||||
/// 0x29 u8 governance_mode (GovernanceMode)
|
||||
/// 0x2A u16 tool_call_count
|
||||
/// 0x2C u32 total_cost_microdollars
|
||||
/// 0x30 u32 total_latency_ms
|
||||
/// 0x34 u32 total_tokens
|
||||
/// 0x38 u16 retry_count
|
||||
/// 0x3A u16 section_count
|
||||
/// 0x3C u32 total_bundle_size
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct WitnessHeader {
|
||||
/// Magic bytes: WITNESS_MAGIC.
|
||||
pub magic: u32,
|
||||
/// Format version (currently 1).
|
||||
pub version: u16,
|
||||
/// Bitfield flags (WIT_SIGNED, WIT_HAS_SPEC, etc.).
|
||||
pub flags: u16,
|
||||
/// Unique task identifier (UUID).
|
||||
pub task_id: [u8; 16],
|
||||
/// SHA-256 of the policy file, truncated to 8 bytes.
|
||||
pub policy_hash: [u8; 8],
|
||||
/// Creation timestamp (nanoseconds since UNIX epoch).
|
||||
pub created_ns: u64,
|
||||
/// Task outcome discriminant.
|
||||
pub outcome: u8,
|
||||
/// Governance mode discriminant.
|
||||
pub governance_mode: u8,
|
||||
/// Number of tool calls recorded.
|
||||
pub tool_call_count: u16,
|
||||
/// Total cost in microdollars (1/1,000,000 USD).
|
||||
pub total_cost_microdollars: u32,
|
||||
/// Total wall-clock latency in milliseconds.
|
||||
pub total_latency_ms: u32,
|
||||
/// Total tokens consumed (prompt + completion).
|
||||
pub total_tokens: u32,
|
||||
/// Number of retries across all tool calls.
|
||||
pub retry_count: u16,
|
||||
/// Number of TLV sections in the payload.
|
||||
pub section_count: u16,
|
||||
/// Total size of the entire witness bundle (header + payload + sig).
|
||||
pub total_bundle_size: u32,
|
||||
}
|
||||
|
||||
// Compile-time size assertion.
|
||||
const _: () = assert!(core::mem::size_of::<WitnessHeader>() == 64);
|
||||
|
||||
impl WitnessHeader {
|
||||
/// Check magic bytes.
|
||||
pub const fn is_valid_magic(&self) -> bool {
|
||||
self.magic == WITNESS_MAGIC
|
||||
}
|
||||
|
||||
/// Check if the bundle is signed.
|
||||
pub const fn is_signed(&self) -> bool {
|
||||
self.flags & WIT_SIGNED != 0
|
||||
}
|
||||
|
||||
/// Serialize header to a 64-byte array.
|
||||
pub fn to_bytes(&self) -> [u8; WITNESS_HEADER_SIZE] {
|
||||
let mut buf = [0u8; WITNESS_HEADER_SIZE];
|
||||
buf[0..4].copy_from_slice(&self.magic.to_le_bytes());
|
||||
buf[4..6].copy_from_slice(&self.version.to_le_bytes());
|
||||
buf[6..8].copy_from_slice(&self.flags.to_le_bytes());
|
||||
buf[8..24].copy_from_slice(&self.task_id);
|
||||
buf[24..32].copy_from_slice(&self.policy_hash);
|
||||
buf[32..40].copy_from_slice(&self.created_ns.to_le_bytes());
|
||||
buf[40] = self.outcome;
|
||||
buf[41] = self.governance_mode;
|
||||
buf[42..44].copy_from_slice(&self.tool_call_count.to_le_bytes());
|
||||
buf[44..48].copy_from_slice(&self.total_cost_microdollars.to_le_bytes());
|
||||
buf[48..52].copy_from_slice(&self.total_latency_ms.to_le_bytes());
|
||||
buf[52..56].copy_from_slice(&self.total_tokens.to_le_bytes());
|
||||
buf[56..58].copy_from_slice(&self.retry_count.to_le_bytes());
|
||||
buf[58..60].copy_from_slice(&self.section_count.to_le_bytes());
|
||||
buf[60..64].copy_from_slice(&self.total_bundle_size.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize header from a byte slice (>= 64 bytes).
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, crate::RvfError> {
|
||||
if data.len() < WITNESS_HEADER_SIZE {
|
||||
return Err(crate::RvfError::SizeMismatch {
|
||||
expected: WITNESS_HEADER_SIZE,
|
||||
got: data.len(),
|
||||
});
|
||||
}
|
||||
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
if magic != WITNESS_MAGIC {
|
||||
return Err(crate::RvfError::BadMagic {
|
||||
expected: WITNESS_MAGIC,
|
||||
got: magic,
|
||||
});
|
||||
}
|
||||
let mut task_id = [0u8; 16];
|
||||
task_id.copy_from_slice(&data[8..24]);
|
||||
let mut policy_hash = [0u8; 8];
|
||||
policy_hash.copy_from_slice(&data[24..32]);
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version: u16::from_le_bytes([data[4], data[5]]),
|
||||
flags: u16::from_le_bytes([data[6], data[7]]),
|
||||
task_id,
|
||||
policy_hash,
|
||||
created_ns: u64::from_le_bytes([
|
||||
data[32], data[33], data[34], data[35], data[36], data[37], data[38], data[39],
|
||||
]),
|
||||
outcome: data[40],
|
||||
governance_mode: data[41],
|
||||
tool_call_count: u16::from_le_bytes([data[42], data[43]]),
|
||||
total_cost_microdollars: u32::from_le_bytes([data[44], data[45], data[46], data[47]]),
|
||||
total_latency_ms: u32::from_le_bytes([data[48], data[49], data[50], data[51]]),
|
||||
total_tokens: u32::from_le_bytes([data[52], data[53], data[54], data[55]]),
|
||||
retry_count: u16::from_le_bytes([data[56], data[57]]),
|
||||
section_count: u16::from_le_bytes([data[58], data[59]]),
|
||||
total_bundle_size: u32::from_le_bytes([data[60], data[61], data[62], data[63]]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A single tool call record within a witness trace.
|
||||
///
|
||||
/// Requires the `alloc` feature because the variable-length `action` field
|
||||
/// uses `Vec<u8>`.
|
||||
///
|
||||
/// Variable-length: 32 bytes fixed header + action_len bytes.
|
||||
///
|
||||
/// ```text
|
||||
/// Offset Type Field
|
||||
/// 0x00 u16 action_len
|
||||
/// 0x02 u8 policy_check (PolicyCheck)
|
||||
/// 0x03 u8 _pad
|
||||
/// 0x04 [u8; 8] args_hash (SHA-256 truncated)
|
||||
/// 0x0C [u8; 8] result_hash (SHA-256 truncated)
|
||||
/// 0x14 u32 latency_ms
|
||||
/// 0x18 u32 cost_microdollars
|
||||
/// 0x1C u32 tokens
|
||||
/// 0x20 [u8; action_len] action (UTF-8 tool name)
|
||||
/// ```
|
||||
#[cfg(any(feature = "alloc", test))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ToolCallEntry {
|
||||
/// Tool name / action (e.g. "Bash", "Edit", "Read").
|
||||
pub action: alloc::vec::Vec<u8>,
|
||||
/// SHA-256 of args, truncated to 8 bytes.
|
||||
pub args_hash: [u8; 8],
|
||||
/// SHA-256 of result, truncated to 8 bytes.
|
||||
pub result_hash: [u8; 8],
|
||||
/// Wall-clock latency in milliseconds.
|
||||
pub latency_ms: u32,
|
||||
/// Cost in microdollars.
|
||||
pub cost_microdollars: u32,
|
||||
/// Tokens consumed.
|
||||
pub tokens: u32,
|
||||
/// Policy check result.
|
||||
pub policy_check: PolicyCheck,
|
||||
}
|
||||
|
||||
/// Fixed header size for a ToolCallEntry (before the action string).
|
||||
#[cfg(any(feature = "alloc", test))]
|
||||
pub const TOOL_CALL_FIXED_SIZE: usize = 32;
|
||||
|
||||
#[cfg(any(feature = "alloc", test))]
|
||||
impl ToolCallEntry {
|
||||
/// Total serialized size.
|
||||
pub fn wire_size(&self) -> usize {
|
||||
TOOL_CALL_FIXED_SIZE + self.action.len()
|
||||
}
|
||||
|
||||
/// Serialize to bytes.
|
||||
pub fn to_bytes(&self) -> alloc::vec::Vec<u8> {
|
||||
let mut buf = alloc::vec::Vec::with_capacity(self.wire_size());
|
||||
buf.extend_from_slice(&(self.action.len() as u16).to_le_bytes());
|
||||
buf.push(self.policy_check as u8);
|
||||
buf.push(0); // pad
|
||||
buf.extend_from_slice(&self.args_hash);
|
||||
buf.extend_from_slice(&self.result_hash);
|
||||
buf.extend_from_slice(&self.latency_ms.to_le_bytes());
|
||||
buf.extend_from_slice(&self.cost_microdollars.to_le_bytes());
|
||||
buf.extend_from_slice(&self.tokens.to_le_bytes());
|
||||
buf.extend_from_slice(&self.action);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize from bytes. Returns (entry, bytes_consumed).
|
||||
pub fn from_bytes(data: &[u8]) -> Option<(Self, usize)> {
|
||||
if data.len() < TOOL_CALL_FIXED_SIZE {
|
||||
return None;
|
||||
}
|
||||
let action_len = u16::from_le_bytes([data[0], data[1]]) as usize;
|
||||
let total = TOOL_CALL_FIXED_SIZE + action_len;
|
||||
if data.len() < total {
|
||||
return None;
|
||||
}
|
||||
let policy_check = PolicyCheck::try_from(data[2]).ok()?;
|
||||
let mut args_hash = [0u8; 8];
|
||||
args_hash.copy_from_slice(&data[4..12]);
|
||||
let mut result_hash = [0u8; 8];
|
||||
result_hash.copy_from_slice(&data[12..20]);
|
||||
let latency_ms = u32::from_le_bytes([data[20], data[21], data[22], data[23]]);
|
||||
let cost_microdollars = u32::from_le_bytes([data[24], data[25], data[26], data[27]]);
|
||||
let tokens = u32::from_le_bytes([data[28], data[29], data[30], data[31]]);
|
||||
let action = data[TOOL_CALL_FIXED_SIZE..total].to_vec();
|
||||
|
||||
Some((
|
||||
Self {
|
||||
action,
|
||||
args_hash,
|
||||
result_hash,
|
||||
latency_ms,
|
||||
cost_microdollars,
|
||||
tokens,
|
||||
policy_check,
|
||||
},
|
||||
total,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Capability scorecard — aggregate metrics across witness bundles.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Scorecard {
|
||||
/// Total tasks attempted.
|
||||
pub total_tasks: u32,
|
||||
/// Tasks solved with passing tests.
|
||||
pub solved: u32,
|
||||
/// Tasks that failed (tests don't pass).
|
||||
pub failed: u32,
|
||||
/// Tasks skipped.
|
||||
pub skipped: u32,
|
||||
/// Tasks that errored (infra failure).
|
||||
pub errors: u32,
|
||||
/// Policy violations detected.
|
||||
pub policy_violations: u32,
|
||||
/// Rollbacks performed.
|
||||
pub rollback_count: u32,
|
||||
/// Total cost in microdollars.
|
||||
pub total_cost_microdollars: u64,
|
||||
/// Median latency in milliseconds.
|
||||
pub median_latency_ms: u32,
|
||||
/// 95th percentile latency in milliseconds.
|
||||
pub p95_latency_ms: u32,
|
||||
/// Total tokens consumed.
|
||||
pub total_tokens: u64,
|
||||
/// Total retries across all tasks.
|
||||
pub total_retries: u32,
|
||||
/// Fraction of solved tasks with complete witness bundles.
|
||||
pub evidence_coverage: f32,
|
||||
/// Cost per solved task in microdollars.
|
||||
pub cost_per_solve_microdollars: u32,
|
||||
/// Solve rate (solved / total_tasks).
|
||||
pub solve_rate: f32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
extern crate alloc;
|
||||
|
||||
#[test]
|
||||
fn witness_header_size() {
|
||||
assert_eq!(core::mem::size_of::<WitnessHeader>(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn witness_header_round_trip() {
|
||||
let hdr = WitnessHeader {
|
||||
magic: WITNESS_MAGIC,
|
||||
version: 1,
|
||||
flags: WIT_SIGNED | WIT_HAS_SPEC | WIT_HAS_DIFF,
|
||||
task_id: [0x42; 16],
|
||||
policy_hash: [0xAA; 8],
|
||||
created_ns: 1_700_000_000_000_000_000,
|
||||
outcome: TaskOutcome::Solved as u8,
|
||||
governance_mode: GovernanceMode::Approved as u8,
|
||||
tool_call_count: 12,
|
||||
total_cost_microdollars: 15_000,
|
||||
total_latency_ms: 4_500,
|
||||
total_tokens: 8_000,
|
||||
retry_count: 2,
|
||||
section_count: 3,
|
||||
total_bundle_size: 2048,
|
||||
};
|
||||
let bytes = hdr.to_bytes();
|
||||
assert_eq!(bytes.len(), WITNESS_HEADER_SIZE);
|
||||
let decoded = WitnessHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded, hdr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn witness_header_bad_magic() {
|
||||
let mut bytes = [0u8; 64];
|
||||
bytes[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
|
||||
assert!(WitnessHeader::from_bytes(&bytes).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn witness_header_too_short() {
|
||||
assert!(WitnessHeader::from_bytes(&[0u8; 32]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_outcome_round_trip() {
|
||||
for raw in 0..=3u8 {
|
||||
let o = TaskOutcome::try_from(raw).unwrap();
|
||||
assert_eq!(o as u8, raw);
|
||||
}
|
||||
assert!(TaskOutcome::try_from(4).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn governance_mode_round_trip() {
|
||||
for raw in 0..=2u8 {
|
||||
let g = GovernanceMode::try_from(raw).unwrap();
|
||||
assert_eq!(g as u8, raw);
|
||||
}
|
||||
assert!(GovernanceMode::try_from(3).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_check_round_trip() {
|
||||
for raw in 0..=2u8 {
|
||||
let p = PolicyCheck::try_from(raw).unwrap();
|
||||
assert_eq!(p as u8, raw);
|
||||
}
|
||||
assert!(PolicyCheck::try_from(3).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_call_entry_round_trip() {
|
||||
let entry = ToolCallEntry {
|
||||
action: b"Bash".to_vec(),
|
||||
args_hash: [0x11; 8],
|
||||
result_hash: [0x22; 8],
|
||||
latency_ms: 150,
|
||||
cost_microdollars: 500,
|
||||
tokens: 200,
|
||||
policy_check: PolicyCheck::Allowed,
|
||||
};
|
||||
let bytes = entry.to_bytes();
|
||||
assert_eq!(bytes.len(), TOOL_CALL_FIXED_SIZE + 4);
|
||||
let (decoded, consumed) = ToolCallEntry::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded, entry);
|
||||
assert_eq!(consumed, bytes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_call_entry_too_short() {
|
||||
assert!(ToolCallEntry::from_bytes(&[0u8; 10]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn witness_flags() {
|
||||
let flags = WIT_SIGNED | WIT_HAS_SPEC | WIT_HAS_DIFF | WIT_HAS_TEST_LOG;
|
||||
assert_ne!(flags & WIT_SIGNED, 0);
|
||||
assert_ne!(flags & WIT_HAS_SPEC, 0);
|
||||
assert_eq!(flags & WIT_HAS_PLAN, 0);
|
||||
assert_ne!(flags & WIT_HAS_DIFF, 0);
|
||||
assert_ne!(flags & WIT_HAS_TEST_LOG, 0);
|
||||
assert_eq!(flags & WIT_HAS_POSTMORTEM, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scorecard_default_is_zero() {
|
||||
let s = Scorecard::default();
|
||||
assert_eq!(s.total_tasks, 0);
|
||||
assert_eq!(s.solved, 0);
|
||||
assert_eq!(s.solve_rate, 0.0);
|
||||
assert_eq!(s.evidence_coverage, 0.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user