feat: implement v0.6.0 mutation engine and db_type runtime selection
Rust / build (push) Canceled after 0s
Rust / build (push) Canceled after 0s
This commit is contained in:
1 parent
217dc5f92b
commit
ba768da58e
27 files changed
+2615
-158
No files matched your search
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "shared"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -10,4 +10,4 @@ blake3 = "1"
|
||||
hex = "0.4"
|
||||
base64 = "0.22"
|
||||
rand = "0.8"
|
||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
@@ -1,2 +1,6 @@
|
||||
pub const SESSION_ID_LEN: usize = 32;
|
||||
pub const SALT_LEN: usize = 16;
|
||||
pub const DEFAULT_GENE_SIZE: usize = 512;
|
||||
pub const MAX_GENE_SIZE: usize = 4096;
|
||||
pub const MAX_ENV_RECORDS: usize = 48;
|
||||
pub const MAX_MUTATION_PROGRAM_BYTES: usize = 256;
|
||||
@@ -0,0 +1,381 @@
|
||||
use crate::constants::{DEFAULT_GENE_SIZE, MAX_ENV_RECORDS, MAX_GENE_SIZE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EnvironmentRecord {
|
||||
pub symbol: u16,
|
||||
pub quantity: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GeneState {
|
||||
pub gene: Vec<u8>,
|
||||
pub environment: Vec<EnvironmentRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GeneError {
|
||||
InvalidGeneSize { size: usize },
|
||||
TooManyEnvironmentRecords { len: usize },
|
||||
EnvironmentNotSorted,
|
||||
DuplicateEnvironmentSymbol(u16),
|
||||
ZeroQuantitySymbol(u16),
|
||||
EnvironmentFull,
|
||||
EnvironmentBlobLengthInvalid { len: usize },
|
||||
EnvironmentBlobTooLarge { records: usize },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GeneError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidGeneSize { size } => write!(f, "invalid gene size: {size}"),
|
||||
Self::TooManyEnvironmentRecords { len } => {
|
||||
write!(f, "too many environment records: {len}")
|
||||
}
|
||||
Self::EnvironmentNotSorted => write!(f, "environment records are not sorted"),
|
||||
Self::DuplicateEnvironmentSymbol(symbol) => {
|
||||
write!(f, "duplicate environment symbol: {symbol}")
|
||||
}
|
||||
Self::ZeroQuantitySymbol(symbol) => {
|
||||
write!(f, "environment quantity cannot be zero for symbol {symbol}")
|
||||
}
|
||||
Self::EnvironmentFull => write!(f, "environment is at maximum capacity"),
|
||||
Self::EnvironmentBlobLengthInvalid { len } => {
|
||||
write!(
|
||||
f,
|
||||
"environment blob length must be a multiple of 6, got {len}"
|
||||
)
|
||||
}
|
||||
Self::EnvironmentBlobTooLarge { records } => {
|
||||
write!(f, "environment blob contains too many records: {records}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for GeneError {}
|
||||
|
||||
pub fn new_state(gene_size: usize) -> Result<GeneState, GeneError> {
|
||||
if !(1..=MAX_GENE_SIZE).contains(&gene_size) {
|
||||
return Err(GeneError::InvalidGeneSize { size: gene_size });
|
||||
}
|
||||
Ok(GeneState {
|
||||
gene: vec![0; gene_size],
|
||||
environment: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn default_state() -> GeneState {
|
||||
GeneState {
|
||||
gene: vec![0; DEFAULT_GENE_SIZE],
|
||||
environment: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_state(state: &GeneState) -> Result<(), GeneError> {
|
||||
if !(1..=MAX_GENE_SIZE).contains(&state.gene.len()) {
|
||||
return Err(GeneError::InvalidGeneSize {
|
||||
size: state.gene.len(),
|
||||
});
|
||||
}
|
||||
validate_environment(&state.environment)
|
||||
}
|
||||
|
||||
pub fn get_env_quantity(state: &GeneState, symbol: u16) -> u32 {
|
||||
match state
|
||||
.environment
|
||||
.binary_search_by_key(&symbol, |record| record.symbol)
|
||||
{
|
||||
Ok(i) => state.environment[i].quantity,
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_env_quantity(
|
||||
state: &mut GeneState,
|
||||
symbol: u16,
|
||||
quantity: u32,
|
||||
) -> Result<(), GeneError> {
|
||||
let idx = state
|
||||
.environment
|
||||
.binary_search_by_key(&symbol, |record| record.symbol);
|
||||
match (idx, quantity) {
|
||||
(Ok(i), 0) => {
|
||||
state.environment.remove(i);
|
||||
Ok(())
|
||||
}
|
||||
(Ok(i), qty) => {
|
||||
state.environment[i].quantity = qty;
|
||||
Ok(())
|
||||
}
|
||||
(Err(_), 0) => Ok(()),
|
||||
(Err(i), qty) => {
|
||||
if state.environment.len() >= MAX_ENV_RECORDS {
|
||||
return Err(GeneError::EnvironmentFull);
|
||||
}
|
||||
state.environment.insert(
|
||||
i,
|
||||
EnvironmentRecord {
|
||||
symbol,
|
||||
quantity: qty,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_env_quantity(
|
||||
state: &mut GeneState,
|
||||
symbol: u16,
|
||||
quantity: u32,
|
||||
) -> Result<u32, GeneError> {
|
||||
let current = get_env_quantity(state, symbol);
|
||||
let next = current.saturating_add(quantity);
|
||||
set_env_quantity(state, symbol, next)?;
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub fn sub_env_quantity(
|
||||
state: &mut GeneState,
|
||||
symbol: u16,
|
||||
quantity: u32,
|
||||
) -> Result<u32, GeneError> {
|
||||
let current = get_env_quantity(state, symbol);
|
||||
let next = current.saturating_sub(quantity);
|
||||
set_env_quantity(state, symbol, next)?;
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub fn encode_environment(records: &[EnvironmentRecord]) -> Result<Vec<u8>, GeneError> {
|
||||
validate_environment(records)?;
|
||||
let mut out = Vec::with_capacity(records.len() * 6);
|
||||
for record in records {
|
||||
out.extend_from_slice(&record.symbol.to_le_bytes());
|
||||
out.extend_from_slice(&record.quantity.to_le_bytes());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn decode_environment(blob: &[u8]) -> Result<Vec<EnvironmentRecord>, GeneError> {
|
||||
if blob.len() % 6 != 0 {
|
||||
return Err(GeneError::EnvironmentBlobLengthInvalid { len: blob.len() });
|
||||
}
|
||||
let records_len = blob.len() / 6;
|
||||
if records_len > MAX_ENV_RECORDS {
|
||||
return Err(GeneError::EnvironmentBlobTooLarge {
|
||||
records: records_len,
|
||||
});
|
||||
}
|
||||
|
||||
let mut records = Vec::with_capacity(records_len);
|
||||
let mut i = 0;
|
||||
while i < blob.len() {
|
||||
let symbol = u16::from_le_bytes([blob[i], blob[i + 1]]);
|
||||
let quantity = u32::from_le_bytes([blob[i + 2], blob[i + 3], blob[i + 4], blob[i + 5]]);
|
||||
records.push(EnvironmentRecord { symbol, quantity });
|
||||
i += 6;
|
||||
}
|
||||
validate_environment(&records)?;
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
pub fn commitment(state: &GeneState) -> [u8; 32] {
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(b"chronoseal/gene/v1");
|
||||
h.update(&(state.gene.len() as u32).to_le_bytes());
|
||||
h.update(&state.gene);
|
||||
h.update(&(state.environment.len() as u16).to_le_bytes());
|
||||
for record in &state.environment {
|
||||
h.update(&record.symbol.to_le_bytes());
|
||||
h.update(&record.quantity.to_le_bytes());
|
||||
}
|
||||
*h.finalize().as_bytes()
|
||||
}
|
||||
|
||||
pub fn commitment_hex(state: &GeneState) -> String {
|
||||
hex::encode(commitment(state))
|
||||
}
|
||||
|
||||
fn validate_environment(records: &[EnvironmentRecord]) -> Result<(), GeneError> {
|
||||
if records.len() > MAX_ENV_RECORDS {
|
||||
return Err(GeneError::TooManyEnvironmentRecords { len: records.len() });
|
||||
}
|
||||
let mut prev_symbol: Option<u16> = None;
|
||||
for record in records {
|
||||
if record.quantity == 0 {
|
||||
return Err(GeneError::ZeroQuantitySymbol(record.symbol));
|
||||
}
|
||||
if let Some(prev) = prev_symbol {
|
||||
if record.symbol < prev {
|
||||
return Err(GeneError::EnvironmentNotSorted);
|
||||
}
|
||||
if record.symbol == prev {
|
||||
return Err(GeneError::DuplicateEnvironmentSymbol(record.symbol));
|
||||
}
|
||||
}
|
||||
prev_symbol = Some(record.symbol);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
#[test]
|
||||
fn test_new_state_with_default_size() {
|
||||
let state = new_state(DEFAULT_GENE_SIZE).unwrap();
|
||||
assert_eq!(state.gene.len(), DEFAULT_GENE_SIZE);
|
||||
assert!(state.environment.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_state_rejects_invalid_sizes() {
|
||||
assert!(matches!(
|
||||
new_state(0).unwrap_err(),
|
||||
GeneError::InvalidGeneSize { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
new_state(MAX_GENE_SIZE + 1).unwrap_err(),
|
||||
GeneError::InvalidGeneSize { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_and_get_env_quantity() {
|
||||
let mut state = new_state(8).unwrap();
|
||||
set_env_quantity(&mut state, 42, 7).unwrap();
|
||||
assert_eq!(get_env_quantity(&state, 42), 7);
|
||||
set_env_quantity(&mut state, 42, 0).unwrap();
|
||||
assert_eq!(get_env_quantity(&state, 42), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_env_quantity_saturates() {
|
||||
let mut state = new_state(8).unwrap();
|
||||
set_env_quantity(&mut state, 1, u32::MAX - 3).unwrap();
|
||||
let next = add_env_quantity(&mut state, 1, 99).unwrap();
|
||||
assert_eq!(next, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sub_env_quantity_removes_symbol() {
|
||||
let mut state = new_state(8).unwrap();
|
||||
set_env_quantity(&mut state, 7, 10).unwrap();
|
||||
let next = sub_env_quantity(&mut state, 7, 100).unwrap();
|
||||
assert_eq!(next, 0);
|
||||
assert!(state.environment.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_environment_capacity_limit_is_enforced() {
|
||||
let mut state = new_state(8).unwrap();
|
||||
for symbol in 0..(MAX_ENV_RECORDS as u16) {
|
||||
set_env_quantity(&mut state, symbol, 1).unwrap();
|
||||
}
|
||||
let err = set_env_quantity(&mut state, 500, 1).unwrap_err();
|
||||
assert_eq!(err, GeneError::EnvironmentFull);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode_environment_roundtrip() {
|
||||
let records = vec![
|
||||
EnvironmentRecord {
|
||||
symbol: 3,
|
||||
quantity: 9,
|
||||
},
|
||||
EnvironmentRecord {
|
||||
symbol: 11,
|
||||
quantity: 999,
|
||||
},
|
||||
];
|
||||
let blob = encode_environment(&records).unwrap();
|
||||
let decoded = decode_environment(&blob).unwrap();
|
||||
assert_eq!(decoded, records);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_environment_rejects_unsorted_records() {
|
||||
let mut blob = Vec::new();
|
||||
blob.extend_from_slice(&7u16.to_le_bytes());
|
||||
blob.extend_from_slice(&1u32.to_le_bytes());
|
||||
blob.extend_from_slice(&2u16.to_le_bytes());
|
||||
blob.extend_from_slice(&1u32.to_le_bytes());
|
||||
let err = decode_environment(&blob).unwrap_err();
|
||||
assert_eq!(err, GeneError::EnvironmentNotSorted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_environment_rejects_zero_quantity() {
|
||||
let mut blob = Vec::new();
|
||||
blob.extend_from_slice(&9u16.to_le_bytes());
|
||||
blob.extend_from_slice(&0u32.to_le_bytes());
|
||||
let err = decode_environment(&blob).unwrap_err();
|
||||
assert_eq!(err, GeneError::ZeroQuantitySymbol(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commitment_changes_when_gene_or_environment_changes() {
|
||||
let mut state_a = new_state(16).unwrap();
|
||||
let mut state_b = state_a.clone();
|
||||
assert_eq!(commitment_hex(&state_a), commitment_hex(&state_b));
|
||||
|
||||
state_b.gene[0] = 1;
|
||||
assert_ne!(commitment_hex(&state_a), commitment_hex(&state_b));
|
||||
|
||||
set_env_quantity(&mut state_a, 7, 3).unwrap();
|
||||
assert_ne!(commitment_hex(&state_a), commitment_hex(&state_b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_state_rejects_duplicate_environment_symbols() {
|
||||
let state = GeneState {
|
||||
gene: vec![0; 10],
|
||||
environment: vec![
|
||||
EnvironmentRecord {
|
||||
symbol: 1,
|
||||
quantity: 1,
|
||||
},
|
||||
EnvironmentRecord {
|
||||
symbol: 1,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
validate_state(&state).unwrap_err(),
|
||||
GeneError::DuplicateEnvironmentSymbol(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_driven_randomized_environment_roundtrip() {
|
||||
for seed in 0..32u64 {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
|
||||
let mut state = new_state(32).unwrap();
|
||||
|
||||
for _ in 0..128 {
|
||||
let symbol = rng.gen_range(0u16..200u16);
|
||||
let qty = if rng.gen_bool(0.15) {
|
||||
0
|
||||
} else {
|
||||
rng.gen_range(1u32..100_000u32)
|
||||
};
|
||||
if let Err(err) = set_env_quantity(&mut state, symbol, qty) {
|
||||
assert_eq!(err, GeneError::EnvironmentFull);
|
||||
}
|
||||
validate_state(&state).unwrap();
|
||||
}
|
||||
|
||||
let blob = encode_environment(&state.environment).unwrap();
|
||||
let decoded = decode_environment(&blob).unwrap();
|
||||
assert_eq!(decoded, state.environment);
|
||||
|
||||
let commitment_a = commitment(&state);
|
||||
let commitment_b = commitment(&state.clone());
|
||||
assert_eq!(commitment_a, commitment_b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod constants;
|
||||
pub mod gene;
|
||||
pub mod hashing;
|
||||
pub mod protocol;
|
||||
pub mod vm_extensions;
|
||||
+15
-6
@@ -1,11 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct InitRequest {
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InitResponse {
|
||||
pub session_id: String,
|
||||
pub salt: String,
|
||||
@@ -14,9 +14,12 @@ pub struct InitResponse {
|
||||
pub expires_at: u64,
|
||||
pub heartbeat_min_interval_ms: u64,
|
||||
pub heartbeat_max_interval_ms: u64,
|
||||
pub gene_size: u32,
|
||||
pub mutation_step: u64,
|
||||
pub mutation_order_b64: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct HeartbeatRequest {
|
||||
pub session_id: String,
|
||||
pub prev_hash: String,
|
||||
@@ -24,17 +27,23 @@ pub struct HeartbeatRequest {
|
||||
pub entropy_data: EntropyData,
|
||||
pub stack_state: StackState,
|
||||
pub fingerprint: Fingerprint,
|
||||
pub mutation_step: u64,
|
||||
pub gene_commitment: String,
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HeartbeatResponse {
|
||||
pub status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_salt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_mutation_step: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_mutation_order_b64: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Fingerprint {
|
||||
#[serde(rename = "aspectRatio")]
|
||||
pub aspect_ratio: String,
|
||||
@@ -44,7 +53,7 @@ pub struct Fingerprint {
|
||||
pub hardware_concurrency: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct EntropyData {
|
||||
pub events: Vec<MouseEvent>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,722 @@
|
||||
use crate::{
|
||||
constants::{MAX_GENE_SIZE, MAX_MUTATION_PROGRAM_BYTES},
|
||||
gene::{
|
||||
add_env_quantity, get_env_quantity, sub_env_quantity, validate_state, GeneError, GeneState,
|
||||
},
|
||||
};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Stack-machine mutation opcodes (v0.6.0).
|
||||
//
|
||||
// NOTE: stack effect notation:
|
||||
// +1 => pushes one u32
|
||||
// -1 => pops one u32
|
||||
// 0 => net-zero (or no stack interaction)
|
||||
//
|
||||
// Security/performance notes:
|
||||
// - All index operands are normalized with modulo to avoid panics.
|
||||
// - Program size is bounded by MAX_MUTATION_PROGRAM_BYTES.
|
||||
// - Environment arithmetic is saturating and deterministic.
|
||||
// - Hashing uses fixed BLAKE3 commitment and fixed transcription algorithm.
|
||||
pub const OP_GENE_LOAD: u8 = 0x23; // +1
|
||||
pub const OP_GENE_STORE: u8 = 0x24; // -1
|
||||
pub const OP_MUTATE_POINT: u8 = 0x25; // 0
|
||||
pub const OP_INSERT: u8 = 0x26; // -1
|
||||
pub const OP_DELETE: u8 = 0x27; // +1
|
||||
pub const OP_TRANSCRIBE: u8 = 0x28; // +1
|
||||
pub const OP_APPLY_MUTAGEN: u8 = 0x29; // -1
|
||||
pub const OP_FINALIZE_GENE_HASH: u8 = 0x2A; // +1
|
||||
pub const OP_CONSUME: u8 = 0x2B; // 0 (pop amount, push remaining)
|
||||
pub const OP_PRODUCE: u8 = 0x2C; // 0 (pop amount, push resulting quantity)
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MutationOrder {
|
||||
pub step: u64,
|
||||
pub program: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExecutionTrace {
|
||||
pub final_ip: usize,
|
||||
pub final_stack: Vec<u32>,
|
||||
pub final_gene_commitment_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MutationError {
|
||||
ProgramTooLong { len: usize },
|
||||
TruncatedInstruction { opcode: u8, ip: usize },
|
||||
UnknownOpcode(u8),
|
||||
EmptyGene,
|
||||
StackUnderflow { opcode: u8, ip: usize },
|
||||
GeneFull { current_len: usize },
|
||||
Base64(base64::DecodeError),
|
||||
Gene(GeneError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MutationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ProgramTooLong { len } => write!(f, "mutation program too long: {len} bytes"),
|
||||
Self::TruncatedInstruction { opcode, ip } => {
|
||||
write!(f, "truncated instruction {opcode:#04x} at ip={ip}")
|
||||
}
|
||||
Self::UnknownOpcode(opcode) => write!(f, "unknown mutation opcode: {opcode:#04x}"),
|
||||
Self::EmptyGene => write!(f, "cannot mutate an empty gene"),
|
||||
Self::StackUnderflow { opcode, ip } => {
|
||||
write!(f, "stack underflow in opcode {opcode:#04x} at ip={ip}")
|
||||
}
|
||||
Self::GeneFull { current_len } => {
|
||||
write!(f, "cannot insert; gene already at max size ({current_len})")
|
||||
}
|
||||
Self::Base64(err) => write!(f, "invalid base64 mutation order: {err}"),
|
||||
Self::Gene(err) => write!(f, "{err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MutationError {}
|
||||
|
||||
impl From<GeneError> for MutationError {
|
||||
fn from(value: GeneError) -> Self {
|
||||
Self::Gene(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_order_b64(order: &MutationOrder) -> String {
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &order.program)
|
||||
}
|
||||
|
||||
pub fn decode_order_b64(step: u64, b64: &str) -> Result<MutationOrder, MutationError> {
|
||||
let program = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64)
|
||||
.map_err(MutationError::Base64)?;
|
||||
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
|
||||
return Err(MutationError::ProgramTooLong { len: program.len() });
|
||||
}
|
||||
Ok(MutationOrder { step, program })
|
||||
}
|
||||
|
||||
pub fn generate_order(step: u64, gene_size: usize) -> MutationOrder {
|
||||
let mut rng = rand::thread_rng();
|
||||
generate_order_with_rng(&mut rng, step, gene_size)
|
||||
}
|
||||
|
||||
pub fn generate_order_with_rng<R: Rng + ?Sized>(
|
||||
rng: &mut R,
|
||||
step: u64,
|
||||
gene_size: usize,
|
||||
) -> MutationOrder {
|
||||
let mut program = Vec::with_capacity(96);
|
||||
let mut stack_depth: i32 = 0;
|
||||
let mut estimated_gene_len = gene_size.clamp(1, MAX_GENE_SIZE);
|
||||
let ops = rng.gen_range(8usize..=18usize);
|
||||
|
||||
for _ in 0..ops {
|
||||
let op = if stack_depth <= 0 {
|
||||
rng.gen_range(0u8..3u8)
|
||||
} else {
|
||||
rng.gen_range(0u8..10u8)
|
||||
};
|
||||
match op {
|
||||
// Pushers
|
||||
0 => {
|
||||
program.push(OP_GENE_LOAD);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
stack_depth += 1;
|
||||
}
|
||||
1 => {
|
||||
program.push(OP_TRANSCRIBE);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
program.push(rng.gen_range(1u8..=16u8));
|
||||
stack_depth += 1;
|
||||
}
|
||||
2 => {
|
||||
program.push(OP_FINALIZE_GENE_HASH);
|
||||
stack_depth += 1;
|
||||
}
|
||||
// Consumers
|
||||
3 => {
|
||||
if stack_depth > 0 {
|
||||
program.push(OP_GENE_STORE);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
stack_depth -= 1;
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
program.push(OP_MUTATE_POINT);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
program.push(rng.r#gen::<u8>());
|
||||
}
|
||||
5 => {
|
||||
if stack_depth > 0 && estimated_gene_len < MAX_GENE_SIZE {
|
||||
program.push(OP_INSERT);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
stack_depth -= 1;
|
||||
estimated_gene_len += 1;
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
program.push(OP_DELETE);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
stack_depth += 1;
|
||||
if estimated_gene_len > 1 {
|
||||
estimated_gene_len -= 1;
|
||||
}
|
||||
}
|
||||
7 => {
|
||||
if stack_depth > 0 {
|
||||
program.push(OP_APPLY_MUTAGEN);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
stack_depth -= 1;
|
||||
}
|
||||
}
|
||||
8 => {
|
||||
if stack_depth > 0 {
|
||||
program.push(OP_CONSUME);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if stack_depth > 0 {
|
||||
program.push(OP_PRODUCE);
|
||||
push_u16(&mut program, rng.r#gen::<u16>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MutationOrder { step, program }
|
||||
}
|
||||
|
||||
pub fn apply_program_clone(state: &GeneState, program: &[u8]) -> Result<GeneState, MutationError> {
|
||||
let mut next = state.clone();
|
||||
apply_program(&mut next, program)?;
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub fn apply_program(state: &mut GeneState, program: &[u8]) -> Result<(), MutationError> {
|
||||
let _ = execute_program(state, program)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn execute_program(
|
||||
state: &mut GeneState,
|
||||
program: &[u8],
|
||||
) -> Result<ExecutionTrace, MutationError> {
|
||||
if state.gene.is_empty() {
|
||||
return Err(MutationError::EmptyGene);
|
||||
}
|
||||
validate_state(state)?;
|
||||
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
|
||||
return Err(MutationError::ProgramTooLong { len: program.len() });
|
||||
}
|
||||
|
||||
let mut ip = 0usize;
|
||||
let mut stack: Vec<u32> = Vec::with_capacity(16);
|
||||
while ip < program.len() {
|
||||
let opcode_ip = ip;
|
||||
let opcode = take_u8(program, &mut ip, 0x00)?;
|
||||
match opcode {
|
||||
OP_GENE_LOAD => {
|
||||
let idx = take_u16(program, &mut ip, opcode)?;
|
||||
let normalized = normalize_index(idx as usize, state.gene.len());
|
||||
stack.push(state.gene[normalized] as u32);
|
||||
}
|
||||
OP_GENE_STORE => {
|
||||
let idx = take_u16(program, &mut ip, opcode)?;
|
||||
let value = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
|
||||
let normalized = normalize_index(idx as usize, state.gene.len());
|
||||
state.gene[normalized] = value;
|
||||
}
|
||||
OP_MUTATE_POINT => {
|
||||
let idx = take_u16(program, &mut ip, opcode)?;
|
||||
let delta = take_u8(program, &mut ip, opcode)? as i8;
|
||||
let normalized = normalize_index(idx as usize, state.gene.len());
|
||||
state.gene[normalized] = state.gene[normalized].wrapping_add(delta as u8);
|
||||
}
|
||||
OP_INSERT => {
|
||||
let idx = take_u16(program, &mut ip, opcode)?;
|
||||
let value = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
|
||||
if state.gene.len() >= MAX_GENE_SIZE {
|
||||
return Err(MutationError::GeneFull {
|
||||
current_len: state.gene.len(),
|
||||
});
|
||||
}
|
||||
let insert_at = (idx as usize).min(state.gene.len());
|
||||
state.gene.insert(insert_at, value);
|
||||
}
|
||||
OP_DELETE => {
|
||||
let idx = take_u16(program, &mut ip, opcode)?;
|
||||
let normalized = normalize_index(idx as usize, state.gene.len());
|
||||
let removed = if state.gene.len() > 1 {
|
||||
state.gene.remove(normalized)
|
||||
} else {
|
||||
let prev = state.gene[0];
|
||||
state.gene[0] = 0;
|
||||
prev
|
||||
};
|
||||
stack.push(removed as u32);
|
||||
}
|
||||
OP_TRANSCRIBE => {
|
||||
let start = take_u16(program, &mut ip, opcode)?;
|
||||
let span = take_u8(program, &mut ip, opcode)?;
|
||||
let transcription = transcribe_window(&state.gene, start as usize, span);
|
||||
stack.push(transcription);
|
||||
}
|
||||
OP_APPLY_MUTAGEN => {
|
||||
let symbol = take_u16(program, &mut ip, opcode)?;
|
||||
let idx = take_u16(program, &mut ip, opcode)?;
|
||||
let stack_mask = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
|
||||
let quantity = get_env_quantity(state, symbol);
|
||||
let mix = ((quantity as u8)
|
||||
^ ((quantity >> 8) as u8)
|
||||
^ ((quantity >> 16) as u8)
|
||||
^ ((quantity >> 24) as u8))
|
||||
^ ((symbol & 0x00ff) as u8)
|
||||
^ ((symbol >> 8) as u8)
|
||||
^ stack_mask;
|
||||
let normalized = normalize_index(idx as usize, state.gene.len());
|
||||
state.gene[normalized] ^= mix;
|
||||
}
|
||||
OP_FINALIZE_GENE_HASH => {
|
||||
let commit = crate::gene::commitment(state);
|
||||
let hash32 = u32::from_le_bytes([commit[0], commit[1], commit[2], commit[3]]);
|
||||
stack.push(hash32);
|
||||
}
|
||||
OP_CONSUME => {
|
||||
let symbol = take_u16(program, &mut ip, opcode)?;
|
||||
let amount = pop_stack(&mut stack, opcode, opcode_ip)?;
|
||||
let left = sub_env_quantity(state, symbol, amount)?;
|
||||
stack.push(left);
|
||||
}
|
||||
OP_PRODUCE => {
|
||||
let symbol = take_u16(program, &mut ip, opcode)?;
|
||||
let amount = pop_stack(&mut stack, opcode, opcode_ip)?;
|
||||
let next = add_env_quantity(state, symbol, amount)?;
|
||||
stack.push(next);
|
||||
}
|
||||
_ => return Err(MutationError::UnknownOpcode(opcode)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionTrace {
|
||||
final_ip: ip,
|
||||
final_stack: stack,
|
||||
final_gene_commitment_hex: crate::gene::commitment_hex(state),
|
||||
})
|
||||
}
|
||||
|
||||
fn transcribe_window(gene: &[u8], start: usize, span: u8) -> u32 {
|
||||
let count = usize::from(span.max(1));
|
||||
let mut acc = 2_166_136_261u32; // FNV offset basis
|
||||
for i in 0..count {
|
||||
let idx = (start + i) % gene.len();
|
||||
acc ^= gene[idx] as u32;
|
||||
acc = acc.wrapping_mul(16_777_619); // FNV prime
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
fn push_u16(buf: &mut Vec<u8>, value: u16) {
|
||||
buf.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
fn take_u8(bytes: &[u8], ip: &mut usize, opcode: u8) -> Result<u8, MutationError> {
|
||||
if *ip >= bytes.len() {
|
||||
return Err(MutationError::TruncatedInstruction { opcode, ip: *ip });
|
||||
}
|
||||
let value = bytes[*ip];
|
||||
*ip += 1;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn take_u16(bytes: &[u8], ip: &mut usize, opcode: u8) -> Result<u16, MutationError> {
|
||||
if *ip + 2 > bytes.len() {
|
||||
return Err(MutationError::TruncatedInstruction { opcode, ip: *ip });
|
||||
}
|
||||
let value = u16::from_le_bytes([bytes[*ip], bytes[*ip + 1]]);
|
||||
*ip += 2;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn pop_stack(stack: &mut Vec<u32>, opcode: u8, ip: usize) -> Result<u32, MutationError> {
|
||||
stack
|
||||
.pop()
|
||||
.ok_or(MutationError::StackUnderflow { opcode, ip })
|
||||
}
|
||||
|
||||
fn normalize_index(idx: usize, len: usize) -> usize {
|
||||
idx % len
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::gene::{commitment, new_state, set_env_quantity};
|
||||
use rand::{Rng, SeedableRng};
|
||||
use std::time::Instant;
|
||||
|
||||
fn u16_bytes(v: u16) -> [u8; 2] {
|
||||
v.to_le_bytes()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_gene_load() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
state.gene = vec![10, 20, 30, 40];
|
||||
let trace = execute_program(&mut state, &[OP_GENE_LOAD, 1, 0]).unwrap();
|
||||
assert_eq!(trace.final_stack, vec![20]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_gene_store() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
state.gene = vec![1, 2, 3, 4];
|
||||
let program = vec![
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0, // stack: [1]
|
||||
OP_GENE_STORE,
|
||||
2,
|
||||
0, // gene[2] <- 1
|
||||
];
|
||||
execute_program(&mut state, &program).unwrap();
|
||||
assert_eq!(state.gene, vec![1, 2, 1, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_mutate_point() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
state.gene[0] = 200;
|
||||
let program = vec![OP_MUTATE_POINT, 0, 0, 100u8];
|
||||
execute_program(&mut state, &program).unwrap();
|
||||
assert_eq!(state.gene[0], 44);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_insert() {
|
||||
let mut state = new_state(3).unwrap();
|
||||
state.gene = vec![10, 20, 30];
|
||||
let program = vec![
|
||||
OP_GENE_LOAD,
|
||||
1,
|
||||
0, // stack: [20]
|
||||
OP_INSERT,
|
||||
0,
|
||||
0, // insert 20 at position 0
|
||||
];
|
||||
execute_program(&mut state, &program).unwrap();
|
||||
assert_eq!(state.gene, vec![20, 10, 20, 30]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_delete() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
state.gene = vec![9, 8, 7, 6];
|
||||
let trace = execute_program(&mut state, &[OP_DELETE, 2, 0]).unwrap();
|
||||
assert_eq!(state.gene, vec![9, 8, 6]);
|
||||
assert_eq!(trace.final_stack, vec![7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_transcribe() {
|
||||
let mut state = new_state(5).unwrap();
|
||||
state.gene = vec![1, 2, 3, 4, 5];
|
||||
let trace = execute_program(&mut state, &[OP_TRANSCRIBE, 1, 0, 3]).unwrap();
|
||||
assert_eq!(trace.final_stack.len(), 1);
|
||||
assert_ne!(trace.final_stack[0], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_apply_mutagen() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
set_env_quantity(&mut state, 7, 0x1234_5678).unwrap();
|
||||
state.gene[1] = 0xAA;
|
||||
let program = vec![
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0, // stack mask source
|
||||
OP_APPLY_MUTAGEN,
|
||||
7,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
];
|
||||
execute_program(&mut state, &program).unwrap();
|
||||
assert_ne!(state.gene[1], 0xAA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_finalize_gene_hash() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
let trace = execute_program(&mut state, &[OP_FINALIZE_GENE_HASH]).unwrap();
|
||||
assert_eq!(trace.final_stack.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_consume() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
set_env_quantity(&mut state, 3, 100).unwrap();
|
||||
let program = vec![
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0, // stack = [0]
|
||||
OP_MUTATE_POINT,
|
||||
0,
|
||||
0,
|
||||
15, // gene[0]=15
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0, // stack=[0,15]
|
||||
OP_CONSUME,
|
||||
3,
|
||||
0, // consume 15
|
||||
];
|
||||
let trace = execute_program(&mut state, &program).unwrap();
|
||||
assert_eq!(get_env_quantity(&state, 3), 85);
|
||||
assert_eq!(trace.final_stack.last().copied().unwrap(), 85);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_opcode_produce() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
set_env_quantity(&mut state, 9, 5).unwrap();
|
||||
let program = vec![
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0, // stack [0]
|
||||
OP_MUTATE_POINT,
|
||||
0,
|
||||
0,
|
||||
10, // gene[0]=10
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0, // stack [0,10]
|
||||
OP_PRODUCE,
|
||||
9,
|
||||
0, // +10
|
||||
];
|
||||
let trace = execute_program(&mut state, &program).unwrap();
|
||||
assert_eq!(get_env_quantity(&state, 9), 15);
|
||||
assert_eq!(trace.final_stack.last().copied().unwrap(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_length_gene_is_rejected() {
|
||||
let mut state = GeneState {
|
||||
gene: vec![],
|
||||
environment: vec![],
|
||||
};
|
||||
let err = execute_program(&mut state, &[OP_FINALIZE_GENE_HASH]).unwrap_err();
|
||||
assert_eq!(err, MutationError::EmptyGene);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_rejects_max_size_gene() {
|
||||
let mut state = new_state(MAX_GENE_SIZE).unwrap();
|
||||
let program = vec![
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0, // push value
|
||||
OP_INSERT,
|
||||
0,
|
||||
0,
|
||||
];
|
||||
let err = execute_program(&mut state, &program).unwrap_err();
|
||||
assert!(matches!(err, MutationError::GeneFull { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_positions_wrap_deterministically() {
|
||||
let mut state_a = new_state(5).unwrap();
|
||||
let mut state_b = new_state(5).unwrap();
|
||||
let max_u16 = u16::MAX;
|
||||
let [a0, a1] = u16_bytes(max_u16);
|
||||
let program = vec![OP_MUTATE_POINT, a0, a1, 1];
|
||||
execute_program(&mut state_a, &program).unwrap();
|
||||
|
||||
let wrapped = (max_u16 as usize % 5) as u16;
|
||||
let [w0, w1] = u16_bytes(wrapped);
|
||||
let wrapped_program = vec![OP_MUTATE_POINT, w0, w1, 1];
|
||||
execute_program(&mut state_b, &wrapped_program).unwrap();
|
||||
assert_eq!(state_a, state_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quantity_underflow_is_saturating() {
|
||||
let mut state = new_state(4).unwrap();
|
||||
set_env_quantity(&mut state, 1, 3).unwrap();
|
||||
state.gene[0] = 8;
|
||||
let program = vec![
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0, // 8
|
||||
OP_CONSUME,
|
||||
1,
|
||||
0, // consume 8 from qty 3 => 0
|
||||
];
|
||||
let trace = execute_program(&mut state, &program).unwrap();
|
||||
assert_eq!(get_env_quantity(&state, 1), 0);
|
||||
assert_eq!(trace.final_stack.last().copied().unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_unknown_opcode() {
|
||||
let mut state = new_state(8).unwrap();
|
||||
let err = execute_program(&mut state, &[0xFF]).unwrap_err();
|
||||
assert_eq!(err, MutationError::UnknownOpcode(0xFF));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_truncated_instruction() {
|
||||
let mut state = new_state(8).unwrap();
|
||||
let err = execute_program(&mut state, &[OP_GENE_LOAD, 1]).unwrap_err();
|
||||
assert!(matches!(err, MutationError::TruncatedInstruction { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_stack_underflow() {
|
||||
let mut state = new_state(8).unwrap();
|
||||
let err = execute_program(&mut state, &[OP_GENE_STORE, 0, 0]).unwrap_err();
|
||||
assert!(matches!(err, MutationError::StackUnderflow { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base64_order_roundtrip() {
|
||||
let order = MutationOrder {
|
||||
step: 17,
|
||||
program: vec![OP_GENE_LOAD, 1, 0, OP_GENE_STORE, 2, 0],
|
||||
};
|
||||
let b64 = encode_order_b64(&order);
|
||||
let decoded = decode_order_b64(order.step, &b64).unwrap();
|
||||
assert_eq!(decoded, order);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_order_is_deterministic_for_seeded_rng() {
|
||||
let mut rng_a = rand::rngs::StdRng::seed_from_u64(99);
|
||||
let mut rng_b = rand::rngs::StdRng::seed_from_u64(99);
|
||||
let order_a = generate_order_with_rng(&mut rng_a, 5, 64);
|
||||
let order_b = generate_order_with_rng(&mut rng_b, 5, 64);
|
||||
assert_eq!(order_a, order_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mutation_chain() {
|
||||
let mut server_state = new_state(32).unwrap();
|
||||
let mut client_state = new_state(32).unwrap();
|
||||
|
||||
let program = vec![
|
||||
OP_GENE_LOAD,
|
||||
0,
|
||||
0,
|
||||
OP_PRODUCE,
|
||||
2,
|
||||
0, // env[2]+=gene[0]
|
||||
OP_GENE_LOAD,
|
||||
1,
|
||||
0,
|
||||
OP_APPLY_MUTAGEN,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0, // mutagen at idx1
|
||||
OP_TRANSCRIBE,
|
||||
0,
|
||||
0,
|
||||
8, // hash window
|
||||
OP_GENE_STORE,
|
||||
2,
|
||||
0, // gene[2]=transcription_low_byte
|
||||
OP_DELETE,
|
||||
0,
|
||||
0, // stack pushes removed
|
||||
OP_INSERT,
|
||||
3,
|
||||
0, // insert removed at position 3
|
||||
OP_FINALIZE_GENE_HASH,
|
||||
];
|
||||
|
||||
let server_trace = execute_program(&mut server_state, &program).unwrap();
|
||||
let client_trace = execute_program(&mut client_state, &program).unwrap();
|
||||
|
||||
assert_eq!(server_state, client_state);
|
||||
assert_eq!(server_trace.final_stack, client_trace.final_stack);
|
||||
assert_eq!(
|
||||
server_trace.final_gene_commitment_hex,
|
||||
client_trace.final_gene_commitment_hex
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_client_parity_across_random_orders() {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(7);
|
||||
for step in 0..128u64 {
|
||||
let order = generate_order_with_rng(&mut rng, step, 128);
|
||||
let mut server_state = new_state(128).unwrap();
|
||||
let mut client_state = new_state(128).unwrap();
|
||||
|
||||
let server_result = execute_program(&mut server_state, &order.program);
|
||||
let client_result = execute_program(&mut client_state, &order.program);
|
||||
assert_eq!(server_result.is_ok(), client_result.is_ok());
|
||||
|
||||
match (server_result, client_result) {
|
||||
(Ok(server_trace), Ok(client_trace)) => {
|
||||
assert_eq!(server_state, client_state);
|
||||
assert_eq!(server_trace.final_stack, client_trace.final_stack);
|
||||
assert_eq!(
|
||||
commitment(&server_state),
|
||||
commitment(&client_state),
|
||||
"step {step}"
|
||||
);
|
||||
}
|
||||
(Err(a), Err(b)) => assert_eq!(a.to_string(), b.to_string()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_style_random_program_bytes_do_not_diverge() {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(2026);
|
||||
for _ in 0..256 {
|
||||
let len = rng.gen_range(1usize..=MAX_MUTATION_PROGRAM_BYTES);
|
||||
let mut program = vec![0u8; len];
|
||||
for b in &mut program {
|
||||
*b = rng.r#gen::<u8>();
|
||||
}
|
||||
|
||||
let mut a = new_state(64).unwrap();
|
||||
let mut b = new_state(64).unwrap();
|
||||
let ra = execute_program(&mut a, &program);
|
||||
let rb = execute_program(&mut b, &program);
|
||||
assert_eq!(ra.is_ok(), rb.is_ok());
|
||||
if ra.is_ok() {
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_performance_smoke_mutation_execution() {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(11);
|
||||
let mut programs = Vec::new();
|
||||
for step in 0..200u64 {
|
||||
programs.push(generate_order_with_rng(&mut rng, step + 1, 512).program);
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
let mut state = new_state(512).unwrap();
|
||||
for program in &programs {
|
||||
let _ = execute_program(&mut state, program);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
// Wide bound for CI variability; this is a regression guard, not a strict benchmark.
|
||||
assert!(
|
||||
elapsed.as_secs_f64() < 2.0,
|
||||
"mutation execution too slow: {elapsed:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user