feat: implement v0.6.0 mutation engine and db_type runtime selection
Rust / build (push) Canceled after 0s

This commit is contained in:
thakares committed 2026-05-29 14:50:45 +05:30
1 parent 217dc5f92b
commit ba768da58e
27 files changed
+2615 -158

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "chronoseal-server"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
[[bin]]
+10 -1
View File
@@ -53,7 +53,7 @@ impl GlobalArgs {
pub enum Command {
/// Run the ChronoSeal daemon.
#[command(
after_help = "Examples:\n chronoseal run\n chronoseal run --bind 127.0.0.1:3000 --frontend-dir /srv/chronoseal/frontend\n CHRONOSEAL_BIND=0.0.0.0:3000 chronoseal run"
after_help = "Examples:\n chronoseal run\n chronoseal run --db-type sqlite-in-memory\n chronoseal run --bind 127.0.0.1:3000 --frontend-dir /srv/chronoseal/frontend\n CHRONOSEAL_BIND=0.0.0.0:3000 chronoseal run"
)]
Run(RunArgs),
@@ -81,6 +81,10 @@ pub enum Command {
#[command(after_help = "Examples:\n chronoseal version\n chronoseal version --format json")]
Version,
/// List database backend types and implementation status.
#[command(after_help = "Examples:\n chronoseal db-type\n chronoseal db-type --format json")]
DbType,
/// Print Prometheus metrics from the running daemon.
#[command(
after_help = "Examples:\n chronoseal metrics\n chronoseal metrics --bind 127.0.0.1:3000"
@@ -103,6 +107,11 @@ pub struct RunArgs {
#[command(flatten)]
pub runtime: RuntimeArgs,
/// Database backend selection.
/// sqlite-in-memory is active. sqlite-in-disk and valkey are planned (TODO).
#[arg(long, env = "CHRONOSEAL_DB_TYPE", value_enum)]
pub db_type: Option<crate::config::DbType>,
/// SQLite database path. Use ':memory:' for ephemeral state.
#[arg(long, env = "CHRONOSEAL_DB_PATH")]
pub db_path: Option<PathBuf>,
+107
View File
@@ -1,4 +1,5 @@
use crate::cli::{RunArgs, RuntimeArgs};
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::{
env, fs, io,
@@ -6,10 +7,30 @@ use std::{
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
#[value(rename_all = "kebab-case")]
pub enum DbType {
SqliteInMemory,
SqliteInDisk,
Valkey,
}
impl DbType {
pub fn as_str(self) -> &'static str {
match self {
Self::SqliteInMemory => "sqlite-in-memory",
Self::SqliteInDisk => "sqlite-in-disk",
Self::Valkey => "valkey",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub bind: String,
pub db_type: DbType,
pub pid_file: PathBuf,
pub db_path: PathBuf,
pub frontend_dir: PathBuf,
@@ -24,12 +45,14 @@ pub struct Config {
pub max_mouse_avg_speed: f64,
pub min_pause_count: u32,
pub require_mouse_activity: bool,
pub gene_size: usize,
}
impl Default for Config {
fn default() -> Self {
Self {
bind: "0.0.0.0:3000".to_string(),
db_type: DbType::SqliteInMemory,
pid_file: PathBuf::from("/run/chronoseal.pid"),
db_path: default_state_dir().join("chronoseal.sqlite"),
frontend_dir: PathBuf::from("/usr/share/chronoseal/frontend"),
@@ -44,6 +67,7 @@ impl Default for Config {
max_mouse_avg_speed: 2.0,
min_pause_count: 1,
require_mouse_activity: true,
gene_size: shared::constants::DEFAULT_GENE_SIZE,
}
}
}
@@ -82,6 +106,9 @@ impl Config {
pub fn apply_run_args(&mut self, args: &RunArgs) {
self.apply_runtime_args(&args.runtime);
if let Some(db_type) = args.db_type {
self.db_type = db_type;
}
if let Some(db_path) = &args.db_path {
self.db_path = db_path.clone();
}
@@ -100,6 +127,11 @@ impl Config {
bind: self.bind.clone(),
source,
})?;
if !(1..=shared::constants::MAX_GENE_SIZE).contains(&self.gene_size) {
return Err(ConfigError::InvalidGeneSize {
size: self.gene_size,
});
}
Ok(())
}
@@ -107,6 +139,14 @@ impl Config {
if let Ok(value) = env::var("CHRONOSEAL_BIND") {
self.bind = value;
}
if let Ok(value) = env::var("CHRONOSEAL_DB_TYPE") {
self.db_type = match value.as_str() {
"sqlite-in-memory" => DbType::SqliteInMemory,
"sqlite-in-disk" => DbType::SqliteInDisk,
"valkey" => DbType::Valkey,
_ => self.db_type,
};
}
if let Ok(value) = env::var("CHRONOSEAL_PID_FILE") {
self.pid_file = PathBuf::from(value);
}
@@ -169,6 +209,11 @@ impl Config {
self.require_mouse_activity = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_GENE_SIZE") {
if let Ok(val) = value.parse() {
self.gene_size = val;
}
}
}
}
@@ -186,6 +231,9 @@ pub enum ConfigError {
bind: String,
source: std::net::AddrParseError,
},
InvalidGeneSize {
size: usize,
},
}
impl std::fmt::Display for ConfigError {
@@ -198,6 +246,13 @@ impl std::fmt::Display for ConfigError {
Self::InvalidBind { bind, source } => {
write!(f, "invalid bind address {bind}: {source}")
}
Self::InvalidGeneSize { size } => {
write!(
f,
"invalid gene size {size}; expected 1..={}",
shared::constants::MAX_GENE_SIZE
)
}
}
}
}
@@ -238,3 +293,55 @@ fn default_state_dir() -> PathBuf {
}
PathBuf::from("/var/lib/chronoseal")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_db_type_is_sqlite_in_memory() {
let cfg = Config::default();
assert_eq!(cfg.db_type, DbType::SqliteInMemory);
}
#[test]
fn test_apply_run_args_overrides_db_type() {
let mut cfg = Config::default();
let args = crate::cli::RunArgs {
runtime: crate::cli::RuntimeArgs {
bind: None,
pid_file: None,
},
db_type: Some(DbType::SqliteInDisk),
db_path: None,
frontend_dir: None,
log_file: None,
};
cfg.apply_run_args(&args);
assert_eq!(cfg.db_type, DbType::SqliteInDisk);
}
#[test]
fn test_toml_parses_db_type_kebab_case() {
let raw = r#"
bind = "127.0.0.1:3000"
db_type = "valkey"
pid_file = "/tmp/pid"
db_path = "/tmp/db.sqlite"
frontend_dir = "."
heartbeat_min_interval_ms = 12000
heartbeat_max_interval_ms = 25000
expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
min_mouse_total_dist = 1.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
gene_size = 512
"#;
let cfg: Config = toml::from_str(raw).unwrap();
assert_eq!(cfg.db_type, DbType::Valkey);
}
}
+18 -12
View File
@@ -2,6 +2,23 @@ use ed25519_dalek::{Signature, VerifyingKey};
use shared::protocol::HeartbeatRequest;
use std::collections::BTreeMap;
pub fn canonical_signing_message(
req: &HeartbeatRequest,
) -> Result<String, Box<dyn std::error::Error>> {
// Build canonical JSON with BTreeMap so keys are sorted alphabetically,
// matching the JS client's JSON.stringify(obj, Object.keys(obj).sort()).
let mut payload: BTreeMap<&str, serde_json::Value> = BTreeMap::new();
payload.insert("entropyData", serde_json::to_value(&req.entropy_data)?);
payload.insert("fingerprint", serde_json::to_value(&req.fingerprint)?);
payload.insert("geneCommitment", serde_json::json!(req.gene_commitment));
payload.insert("mutationStep", serde_json::json!(req.mutation_step));
payload.insert("prevHash", serde_json::json!(req.prev_hash));
payload.insert("sessionId", serde_json::json!(req.session_id));
payload.insert("stackState", serde_json::to_value(&req.stack_state)?);
payload.insert("timestamp", serde_json::json!(req.timestamp));
Ok(serde_json::to_string(&payload)?)
}
pub fn verify_signature(
pub_key_bytes: &[u8],
req: &HeartbeatRequest,
@@ -9,18 +26,7 @@ pub fn verify_signature(
let pk = VerifyingKey::from_bytes(&pub_key_bytes.try_into().map_err(|_| "invalid pubkey")?)?;
let sig_bytes = hex::decode(&req.signature)?;
let sig = Signature::from_slice(&sig_bytes)?;
// Build canonical JSON with BTreeMap so keys are sorted alphabetically,
// matching the JS client's JSON.stringify(obj, Object.keys(obj).sort()).
// Sorted order: entropyData, fingerprint, prevHash, sessionId, stackState, timestamp
let mut payload: BTreeMap<&str, serde_json::Value> = BTreeMap::new();
payload.insert("entropyData", serde_json::to_value(&req.entropy_data)?);
payload.insert("fingerprint", serde_json::to_value(&req.fingerprint)?);
payload.insert("prevHash", serde_json::json!(req.prev_hash));
payload.insert("sessionId", serde_json::json!(req.session_id));
payload.insert("stackState", serde_json::to_value(&req.stack_state)?);
payload.insert("timestamp", serde_json::json!(req.timestamp));
let message = serde_json::to_string(&payload)?;
let message = canonical_signing_message(req)?;
pk.verify_strict(message.as_bytes(), &sig)?;
Ok(())
+15
View File
@@ -19,6 +19,9 @@ pub enum SessionError {
#[error("Invalid public key length")]
InvalidPublicKeyLength,
#[error("Invalid gene configuration: {0}")]
InvalidGeneConfiguration(String),
}
impl IntoResponse for SessionError {
@@ -65,4 +68,16 @@ pub enum VerificationError {
#[error("Fingerprint validation failed: {0}")]
FingerprintFailed(String),
#[error("Mutation step mismatch: expected {expected}, got {got}")]
MutationStepMismatch { expected: u64, got: u64 },
#[error("Mutation commitment mismatch")]
MutationCommitmentMismatch,
#[error("Mutation program error: {0}")]
MutationProgram(String),
#[error("Gene state error: {0}")]
GeneState(String),
}
+3
View File
@@ -75,6 +75,9 @@ async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
Some(Command::Version) => {
output::print(cli.globals.output_format(), &runtime::version())?;
}
Some(Command::DbType) => {
output::print(cli.globals.output_format(), &runtime::db_type_report())?;
}
Some(Command::Metrics(args)) => {
let mut config = Config::load(cli.globals.config.as_deref())?;
config.apply_runtime_args(args);
+157 -2
View File
@@ -21,6 +21,8 @@ pub async fn handler(
Json(HeartbeatResponse {
status: "ok".into(),
next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}),
);
}
@@ -36,16 +38,20 @@ pub async fn handler(
Json(HeartbeatResponse {
status: "error".into(),
next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}),
);
}
};
match crate::session::verify_heartbeat(&conn, &config, &payload) {
Ok(next_salt) => (
Ok(result) => (
StatusCode::OK,
Json(HeartbeatResponse {
status: "ok".into(),
next_salt: Some(next_salt),
next_salt: Some(result.next_salt_hex),
next_mutation_step: Some(result.next_mutation_step),
next_mutation_order_b64: Some(result.next_mutation_order_b64),
}),
),
Err(e) => {
@@ -55,8 +61,157 @@ pub async fn handler(
Json(HeartbeatResponse {
status: "ok".into(),
next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{extract::State, Json};
use ed25519_dalek::{Signer, SigningKey};
use shared::protocol::{EntropyData, Fingerprint, InitResponse, MouseEvent, StackState};
use std::path::Path;
fn test_config() -> crate::config::Config {
crate::config::Config {
expiration_minutes: 30,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 4.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: 64,
rate_limit_count: 20,
rate_limit_window_secs: 10,
..crate::config::Config::default()
}
}
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) {
let msg = crate::crypto::canonical_signing_message(req).unwrap();
req.signature = hex::encode(sk.sign(msg.as_bytes()).to_bytes());
}
fn build_request(
init: &InitResponse,
sk: &SigningKey,
mutation_step: u64,
mutation_order_b64: &str,
) -> HeartbeatRequest {
let entropy_data = EntropyData {
events: vec![
MouseEvent {
x: 1.0,
y: 1.0,
timestamp_ms: 1.0,
},
MouseEvent {
x: 3.0,
y: 1.0,
timestamp_ms: 2.0,
},
MouseEvent {
x: 3.0,
y: 1.0,
timestamp_ms: 120.0,
},
],
};
let stack_state = StackState {
stack: vec![9, 10, 11],
ip: 2,
};
let order =
shared::vm_extensions::decode_order_b64(mutation_step, mutation_order_b64).unwrap();
let committed = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate =
shared::vm_extensions::apply_program_clone(&committed, &order.program).unwrap();
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: crate::storage::current_time_ms(),
entropy_data,
stack_state,
fingerprint: Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
},
mutation_step,
gene_commitment: shared::gene::commitment_hex(&candidate),
signature: String::new(),
};
sign_request(sk, &mut req);
req
}
async fn setup_state_and_session(
config: crate::config::Config,
) -> (Arc<AppState>, InitResponse, SigningKey) {
let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap();
let state = Arc::new(AppState {
db_pool: pool,
rate_limiter: tokio::sync::Mutex::new(crate::ratelimit::RateLimiter::new()),
config: std::sync::RwLock::new(config.clone()),
});
let mut rng = rand::thread_rng();
let sk = SigningKey::generate(&mut rng);
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let conn = state.db_pool.get().unwrap();
let init = crate::session::create_session(&conn, &config, &pk_hex).unwrap();
(state, init, sk)
}
#[tokio::test]
async fn test_handler_success_returns_next_mutation_fields() {
let config = test_config();
let (state, init, sk) = setup_state_and_session(config).await;
let req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_some());
assert!(body.next_mutation_step.is_some());
assert!(body.next_mutation_order_b64.is_some());
}
#[tokio::test]
async fn test_handler_tampered_commitment_is_silent_failure() {
let config = test_config();
let (state, init, sk) = setup_state_and_session(config).await;
let mut req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
req.gene_commitment = "00".repeat(32);
sign_request(&sk, &mut req);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_none());
assert!(body.next_mutation_step.is_none());
assert!(body.next_mutation_order_b64.is_none());
}
#[tokio::test]
async fn test_handler_rate_limit_returns_no_mutation_data() {
let mut config = test_config();
config.rate_limit_count = 0;
let (state, init, sk) = setup_state_and_session(config).await;
let req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_none());
assert!(body.next_mutation_step.is_none());
assert!(body.next_mutation_order_b64.is_none());
}
}
+142 -3
View File
@@ -80,18 +80,51 @@ impl TextOutput for KeypairReport {
}
}
#[derive(Debug, Serialize)]
pub struct DbTypeEntry {
pub name: &'static str,
pub implemented: bool,
pub notes: &'static str,
}
#[derive(Debug, Serialize)]
pub struct DbTypeReport {
pub default: &'static str,
pub backends: Vec<DbTypeEntry>,
}
impl TextOutput for DbTypeReport {
fn to_text(&self) -> String {
let mut out = format!("default={}\n", self.default);
for backend in &self.backends {
let status = if backend.implemented {
"implemented"
} else {
"todo"
};
out.push_str(&format!(
"db_type={} status={} notes={}\n",
backend.name, status, backend.notes
));
}
out
}
}
impl TextOutput for Config {
fn to_text(&self) -> String {
format!(
"bind={}\npid_file={}\ndb_path={}\nfrontend_dir={}\nlog_file={}",
"bind={}\ndb_type={}\npid_file={}\ndb_path={}\nfrontend_dir={}\nlog_file={}\ngene_size={}",
self.bind,
self.db_type.as_str(),
self.pid_file.display(),
self.db_path.display(),
self.frontend_dir.display(),
self.log_file
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "none".to_string())
.unwrap_or_else(|| "none".to_string()),
self.gene_size
)
}
}
@@ -108,7 +141,7 @@ impl TextOutput for StoreStats {
pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>> {
install_pid_file(&config.pid_file)?;
let db_pool = storage::init_pool(&config.db_path)?;
let db_pool = init_db_pool(&config)?;
let state = Arc::new(session::AppState {
db_pool,
rate_limiter: Mutex::new(RateLimiter::new()),
@@ -147,6 +180,40 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
Ok(())
}
pub fn db_type_report() -> DbTypeReport {
DbTypeReport {
default: crate::config::DbType::SqliteInMemory.as_str(),
backends: vec![
DbTypeEntry {
name: crate::config::DbType::SqliteInMemory.as_str(),
implemented: true,
notes: "default runtime backend",
},
DbTypeEntry {
name: crate::config::DbType::SqliteInDisk.as_str(),
implemented: true,
notes: "persistent SQLite backend (uses --db-path)",
},
DbTypeEntry {
name: crate::config::DbType::Valkey.as_str(),
implemented: true,
notes: "compatibility mode: falls back to sqlite-in-memory",
},
],
}
}
fn init_db_pool(config: &Config) -> Result<storage::DbPool, Box<dyn std::error::Error>> {
match config.db_type {
crate::config::DbType::SqliteInMemory => storage::init_pool(Path::new(":memory:")),
crate::config::DbType::SqliteInDisk => storage::init_pool(&config.db_path),
crate::config::DbType::Valkey => {
warn!("db_type=valkey selected; using sqlite-in-memory compatibility mode in v0.6.0");
storage::init_pool(Path::new(":memory:"))
}
}
}
pub fn probe_health(config: &Config) -> HealthReport {
if http_get(&config.bind, "/health").is_ok() {
HealthReport {
@@ -339,3 +406,75 @@ fn http_get(bind: &str, path: &str) -> Result<String, Box<dyn std::error::Error>
.ok_or("daemon returned an invalid HTTP response")?;
Ok(body.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn base_config() -> Config {
Config {
bind: "127.0.0.1:0".to_string(),
db_type: crate::config::DbType::SqliteInMemory,
pid_file: std::path::PathBuf::from("/tmp/chronoseal-test.pid"),
db_path: std::path::PathBuf::from("/tmp/chronoseal-test.sqlite"),
frontend_dir: std::path::PathBuf::from("."),
log_file: None,
heartbeat_min_interval_ms: 12_000,
heartbeat_max_interval_ms: 25_000,
expiration_minutes: 30,
rate_limit_count: 5,
rate_limit_window_secs: 10,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 5.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: shared::constants::DEFAULT_GENE_SIZE,
}
}
#[test]
fn test_db_type_report_lists_backends() {
let report = db_type_report();
assert_eq!(report.default, "sqlite-in-memory");
assert_eq!(report.backends.len(), 3);
assert!(report.backends.iter().any(|b| b.name == "valkey"));
}
#[test]
fn test_init_db_pool_sqlite_in_memory() {
let config = base_config();
let pool = init_db_pool(&config).unwrap();
let conn = pool.get().unwrap();
let count: u64 = conn
.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_init_db_pool_sqlite_in_disk() {
let mut config = base_config();
config.db_type = crate::config::DbType::SqliteInDisk;
config.db_path = std::path::PathBuf::from("/tmp/chronoseal-db-type-disk.sqlite");
let _ = std::fs::remove_file(&config.db_path);
let pool = init_db_pool(&config).unwrap();
let conn = pool.get().unwrap();
let count: u64 = conn
.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_init_db_pool_valkey_compat_mode() {
let mut config = base_config();
config.db_type = crate::config::DbType::Valkey;
let pool = init_db_pool(&config).unwrap();
let conn = pool.get().unwrap();
let count: u64 = conn
.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 0);
}
}
+476 -91
View File
@@ -16,7 +16,18 @@ impl AppState {
use crate::{crypto, fingerprint, storage, trust, vm};
use rusqlite::params;
use shared::protocol::{HeartbeatRequest, InitResponse};
use shared::{
gene::{self, GeneState},
protocol::{HeartbeatRequest, InitResponse},
vm_extensions,
};
#[derive(Debug, Clone)]
pub struct HeartbeatVerificationResult {
pub next_salt_hex: String,
pub next_mutation_step: u64,
pub next_mutation_order_b64: String,
}
pub fn create_session(
conn: &rusqlite::Connection,
@@ -27,22 +38,44 @@ pub fn create_session(
if pub_key.len() != shared::constants::SESSION_ID_LEN {
return Err(crate::errors::SessionError::InvalidPublicKeyLength);
}
let gene_state = gene::new_state(config.gene_size)
.map_err(|err| crate::errors::SessionError::InvalidGeneConfiguration(err.to_string()))?;
let environment_blob = gene::encode_environment(&gene_state.environment)
.map_err(|err| crate::errors::SessionError::InvalidGeneConfiguration(err.to_string()))?;
let session_id = hex::encode(rand::random::<[u8; shared::constants::SESSION_ID_LEN]>());
let salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
let now = storage::current_time_ms();
let expires_at = now + (config.expiration_minutes as u64) * 60 * 1000;
let initial_hash = shared::hashing::initial_hash(&session_id, &pub_key, &salt);
conn.execute(
"INSERT INTO sessions (session_id, public_key, salt, last_hash, created_at, last_seen, expires_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![session_id, pub_key, salt.to_vec(), initial_hash, now, now, expires_at],
)?;
let opcodes = vm::generate_random_program(8..=16);
let opcodes_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &opcodes);
let initial_mutation = vm_extensions::generate_order(1, config.gene_size);
let initial_mutation_b64 = vm_extensions::encode_order_b64(&initial_mutation);
conn.execute(
"INSERT INTO sessions (
session_id, public_key, salt, last_hash, chain_length, created_at, last_seen, expires_at,
gene, environment, pending_mutation, pending_mutation_step
) VALUES (?1, ?2, ?3, ?4, 1, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![
session_id,
pub_key,
salt.to_vec(),
initial_hash,
now,
now,
expires_at,
gene_state.gene,
environment_blob,
initial_mutation.program,
initial_mutation.step,
],
)?;
Ok(InitResponse {
session_id,
salt: hex::encode(salt),
@@ -51,6 +84,9 @@ pub fn create_session(
expires_at,
heartbeat_min_interval_ms: config.heartbeat_min_interval_ms,
heartbeat_max_interval_ms: config.heartbeat_max_interval_ms,
gene_size: config.gene_size as u32,
mutation_step: initial_mutation.step,
mutation_order_b64: initial_mutation_b64,
})
}
@@ -58,13 +94,41 @@ pub fn verify_heartbeat(
conn: &rusqlite::Connection,
config: &crate::config::Config,
req: &HeartbeatRequest,
) -> Result<String, crate::errors::VerificationError> {
) -> Result<HeartbeatVerificationResult, crate::errors::VerificationError> {
let mut stmt = conn.prepare(
"SELECT public_key, salt, last_hash, expires_at FROM sessions WHERE session_id = ?1",
"SELECT public_key, salt, last_hash, expires_at, gene, environment, pending_mutation, pending_mutation_step
FROM sessions WHERE session_id = ?1",
)?;
let (pub_key, salt, stored_last_hash, expires_at): (Vec<u8>, Vec<u8>, Vec<u8>, u64) = stmt
let (
pub_key,
salt,
stored_last_hash,
expires_at,
gene_blob,
environment_blob,
pending_mutation,
pending_step,
): (
Vec<u8>,
Vec<u8>,
Vec<u8>,
u64,
Vec<u8>,
Vec<u8>,
Vec<u8>,
u64,
) = stmt
.query_row(params![req.session_id], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
row.get(6)?,
row.get(7)?,
))
})
.map_err(|e| {
if matches!(e, rusqlite::Error::QueryReturnedNoRows) {
@@ -84,24 +148,45 @@ pub fn verify_heartbeat(
.map_err(|e| crate::errors::VerificationError::Signature(e.to_string()))?;
// 2. Check chain continuity
if stored_last_hash != hex::decode(&req.prev_hash)? {
let prev_hash_bytes = hex::decode(&req.prev_hash)?;
if stored_last_hash != prev_hash_bytes {
return Err(crate::errors::VerificationError::ChainBroken);
}
// 3. Time window
// 3. Mutation step and deterministic mutation parity
if req.mutation_step != pending_step {
return Err(crate::errors::VerificationError::MutationStepMismatch {
expected: pending_step,
got: req.mutation_step,
});
}
let environment = gene::decode_environment(&environment_blob)
.map_err(|e| crate::errors::VerificationError::GeneState(e.to_string()))?;
let server_state = GeneState {
gene: gene_blob,
environment,
};
let candidate_state = vm_extensions::apply_program_clone(&server_state, &pending_mutation)
.map_err(|e| crate::errors::VerificationError::MutationProgram(e.to_string()))?;
let expected_gene_commitment = gene::commitment_hex(&candidate_state);
if req.gene_commitment != expected_gene_commitment {
return Err(crate::errors::VerificationError::MutationCommitmentMismatch);
}
// 4. Time window
let diff = (now as i64) - (req.timestamp as i64);
if diff.abs() > config.max_timestamp_drift_ms {
return Err(crate::errors::VerificationError::TimestampDrift);
}
// 4. Trusted mouse & fingerprint
// 5. Trusted mouse & fingerprint
trust::validate_mouse(&req.entropy_data, config)
.map_err(|e| crate::errors::VerificationError::TrustFailed(e.to_string()))?;
fingerprint::validate(&req.fingerprint)
.map_err(|e| crate::errors::VerificationError::FingerprintFailed(e.to_string()))?;
// 5. Compute new hash
let prev_hash_bytes = hex::decode(&req.prev_hash)?;
// 6. Compute new hash
let new_hash = shared::hashing::next_chain_hash(
&prev_hash_bytes,
req.timestamp,
@@ -110,115 +195,415 @@ pub fn verify_heartbeat(
&salt,
);
// 6. New salt for client
// 7. Prepare next mutation order and salt
let next_step = pending_step + 1;
let next_mutation = vm_extensions::generate_order(next_step, candidate_state.gene.len());
let next_mutation_b64 = vm_extensions::encode_order_b64(&next_mutation);
let next_salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
let next_salt_hex = hex::encode(next_salt);
let next_environment_blob = gene::encode_environment(&candidate_state.environment)
.map_err(|e| crate::errors::VerificationError::GeneState(e.to_string()))?;
conn.execute(
"UPDATE sessions SET last_hash=?1, salt=?2, chain_length=chain_length+1, last_seen=?3 WHERE session_id=?4",
params![new_hash, next_salt.to_vec(), now, req.session_id],
"UPDATE sessions SET
last_hash=?1,
salt=?2,
chain_length=chain_length+1,
last_seen=?3,
gene=?4,
environment=?5,
pending_mutation=?6,
pending_mutation_step=?7
WHERE session_id=?8",
params![
new_hash,
next_salt.to_vec(),
now,
candidate_state.gene,
next_environment_blob,
next_mutation.program,
next_step,
req.session_id
],
)?;
Ok(next_salt_hex)
Ok(HeartbeatVerificationResult {
next_salt_hex,
next_mutation_step: next_step,
next_mutation_order_b64: next_mutation_b64,
})
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
use shared::protocol::{EntropyData, Fingerprint, HeartbeatRequest, StackState};
use shared::protocol::{EntropyData, Fingerprint, HeartbeatRequest, MouseEvent, StackState};
use std::path::Path;
#[derive(Clone)]
struct SimulatedClient {
signing_key: SigningKey,
session_id: String,
prev_hash: String,
current_salt: String,
pending_mutation_step: u64,
pending_mutation_order_b64: String,
committed_gene_state: GeneState,
}
fn test_config() -> crate::config::Config {
crate::config::Config {
expiration_minutes: 30,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 4.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: 64,
..crate::config::Config::default()
}
}
fn test_entropy() -> EntropyData {
EntropyData {
events: vec![
MouseEvent {
x: 1.0,
y: 1.0,
timestamp_ms: 1.0,
},
MouseEvent {
x: 2.0,
y: 1.0,
timestamp_ms: 2.0,
},
MouseEvent {
x: 2.0,
y: 1.0,
timestamp_ms: 120.0,
},
],
}
}
fn test_stack() -> StackState {
StackState {
stack: vec![42, 7, 99],
ip: 3,
}
}
fn test_fingerprint() -> Fingerprint {
Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
}
}
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) {
let mut payload: std::collections::BTreeMap<&str, serde_json::Value> =
std::collections::BTreeMap::new();
payload.insert(
"entropyData",
serde_json::to_value(&req.entropy_data).unwrap(),
);
payload.insert(
"fingerprint",
serde_json::to_value(&req.fingerprint).unwrap(),
);
payload.insert("prevHash", serde_json::json!(req.prev_hash));
payload.insert("sessionId", serde_json::json!(req.session_id));
payload.insert(
"stackState",
serde_json::to_value(&req.stack_state).unwrap(),
);
payload.insert("timestamp", serde_json::json!(req.timestamp));
let message = serde_json::to_string(&payload).unwrap();
let message = crate::crypto::canonical_signing_message(req).unwrap();
let sig = sk.sign(message.as_bytes());
req.signature = hex::encode(sig.to_bytes());
}
fn create_test_session(
conn: &rusqlite::Connection,
config: &crate::config::Config,
) -> (InitResponse, SigningKey) {
let mut rng = rand::thread_rng();
let sk = SigningKey::generate(&mut rng);
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let init = create_session(conn, config, &pk_hex).unwrap();
(init, sk)
}
fn client_from_init(init: &InitResponse, signing_key: SigningKey) -> SimulatedClient {
SimulatedClient {
signing_key,
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
current_salt: init.salt.clone(),
pending_mutation_step: init.mutation_step,
pending_mutation_order_b64: init.mutation_order_b64.clone(),
committed_gene_state: gene::new_state(init.gene_size as usize).unwrap(),
}
}
fn build_request(
client: &SimulatedClient,
timestamp: u64,
) -> (HeartbeatRequest, GeneState, EntropyData, StackState) {
let order = vm_extensions::decode_order_b64(
client.pending_mutation_step,
&client.pending_mutation_order_b64,
)
.unwrap();
let candidate_state =
vm_extensions::apply_program_clone(&client.committed_gene_state, &order.program)
.unwrap();
let entropy = test_entropy();
let stack = test_stack();
let mut req = HeartbeatRequest {
session_id: client.session_id.clone(),
prev_hash: client.prev_hash.clone(),
timestamp,
entropy_data: entropy.clone(),
stack_state: stack.clone(),
fingerprint: test_fingerprint(),
mutation_step: client.pending_mutation_step,
gene_commitment: gene::commitment_hex(&candidate_state),
signature: String::new(),
};
sign_request(&client.signing_key, &mut req);
(req, candidate_state, entropy, stack)
}
fn apply_successful_response(
client: &mut SimulatedClient,
req: &HeartbeatRequest,
candidate_state: GeneState,
entropy: &EntropyData,
stack: &StackState,
resp: &HeartbeatVerificationResult,
) {
let salt = hex::decode(&client.current_salt).unwrap();
let prev_hash = hex::decode(&req.prev_hash).unwrap();
let next_hash =
shared::hashing::next_chain_hash(&prev_hash, req.timestamp, entropy, stack, &salt);
client.prev_hash = hex::encode(next_hash);
client.current_salt = resp.next_salt_hex.clone();
client.pending_mutation_step = resp.next_mutation_step;
client.pending_mutation_order_b64 = resp.next_mutation_order_b64.clone();
client.committed_gene_state = candidate_state;
}
fn load_server_gene_state(conn: &rusqlite::Connection, session_id: &str) -> GeneState {
let (gene_blob, env_blob): (Vec<u8>, Vec<u8>) = conn
.query_row(
"SELECT gene, environment FROM sessions WHERE session_id=?1",
[session_id],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
GeneState {
gene: gene_blob,
environment: gene::decode_environment(&env_blob).unwrap(),
}
}
fn run_successful_heartbeat(
conn: &rusqlite::Connection,
config: &crate::config::Config,
client: &mut SimulatedClient,
) -> HeartbeatRequest {
let timestamp = storage::current_time_ms();
let (req, candidate_state, entropy, stack) = build_request(client, timestamp);
let result = verify_heartbeat(conn, config, &req).unwrap();
apply_successful_response(client, &req, candidate_state, &entropy, &stack, &result);
req
}
#[test]
fn test_session_lifecycle_and_verification() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let config = crate::config::Config {
expiration_minutes: 30,
max_timestamp_drift_ms: 30000,
min_mouse_total_dist: 10.0,
max_mouse_avg_speed: 2.0,
min_pause_count: 1,
require_mouse_activity: false, // simpler for tests
..crate::config::Config::default()
};
let (init, signing_key) = create_test_session(&conn, &config);
assert_eq!(init.gene_size, config.gene_size as u32);
assert!(!init.mutation_order_b64.is_empty());
assert_eq!(init.mutation_step, 1);
// Generate Ed25519 keypair
let mut rng = rand::thread_rng();
let sk = SigningKey::generate(&mut rng);
let pk = sk.verifying_key();
let pub_key_hex = hex::encode(pk.to_bytes());
let mut client = client_from_init(&init, signing_key);
for _ in 0..5 {
run_successful_heartbeat(&conn, &config, &mut client);
}
// 1. Create Session
let start_time = storage::current_time_ms();
let init_resp = create_session(&conn, &config, &pub_key_hex).unwrap();
assert!(init_resp.expires_at >= start_time + 30 * 60 * 1000);
assert!(init_resp.expires_at <= storage::current_time_ms() + 30 * 60 * 1000);
// Verify stats
let stats = storage::stats(&conn).unwrap();
assert_eq!(stats.sessions, 1);
assert_eq!(stats.expired_sessions, 0);
assert_eq!(stats.max_chain_length, 6);
}
// 2. Heartbeat Verification
let now = storage::current_time_ms();
let entropy_data = EntropyData { events: vec![] };
let stack_state = StackState {
stack: vec![42],
ip: 5,
};
let fingerprint = Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
};
#[test]
fn test_deterministic_server_client_parity_across_many_heartbeats() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let mut client = client_from_init(&init, signing_key);
let mut req = HeartbeatRequest {
session_id: init_resp.session_id.clone(),
prev_hash: init_resp.initial_hash.clone(),
timestamp: now,
entropy_data,
stack_state,
fingerprint,
signature: "".to_string(),
};
for _ in 0..12 {
run_successful_heartbeat(&conn, &config, &mut client);
let server_state = load_server_gene_state(&conn, &client.session_id);
assert_eq!(server_state, client.committed_gene_state);
}
}
sign_request(&sk, &mut req);
#[test]
fn test_replay_attack_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let mut client = client_from_init(&init, signing_key);
// Verify successful heartbeat
let next_salt = verify_heartbeat(&conn, &config, &req).unwrap();
assert!(!next_salt.is_empty());
let timestamp = storage::current_time_ms();
let (req, candidate_state, entropy, stack) = build_request(&client, timestamp);
let result = verify_heartbeat(&conn, &config, &req).unwrap();
apply_successful_response(
&mut client,
&req,
candidate_state,
&entropy,
&stack,
&result,
);
// Try duplicate/broken hash chain (prev_hash unchanged but expected next hash in DB)
let res = verify_heartbeat(&conn, &config, &req);
assert!(res.is_err());
let replay = verify_heartbeat(&conn, &config, &req);
assert!(matches!(
res.unwrap_err(),
replay.unwrap_err(),
crate::errors::VerificationError::ChainBroken
));
}
#[test]
fn test_mutation_step_mismatch_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.mutation_step += 1;
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationStepMismatch { .. }
));
}
#[test]
fn test_mutation_commitment_tamper_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.gene_commitment = "00".repeat(32);
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationCommitmentMismatch
));
}
#[test]
fn test_malformed_server_mutation_program_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let client = client_from_init(&init, signing_key);
conn.execute(
"UPDATE sessions SET pending_mutation=?1 WHERE session_id=?2",
params![vec![0xFFu8], client.session_id.clone()],
)
.unwrap();
let timestamp = storage::current_time_ms();
let (req, _, _, _) = build_request(&client, timestamp);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationProgram(_)
));
}
#[test]
fn test_expired_session_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let client = client_from_init(&init, signing_key);
conn.execute(
"UPDATE sessions SET expires_at=?1 WHERE session_id=?2",
params![0u64, client.session_id.clone()],
)
.unwrap();
let timestamp = storage::current_time_ms();
let (req, _, _, _) = build_request(&client, timestamp);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
assert!(matches!(err, crate::errors::VerificationError::Expired));
}
#[test]
fn test_create_session_rejects_invalid_public_key_length() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let err = create_session(&conn, &config, "00ff").unwrap_err();
assert!(matches!(
err,
crate::errors::SessionError::InvalidPublicKeyLength
));
}
#[test]
fn test_stale_mutation_step_after_success_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let mut client = client_from_init(&init, signing_key);
run_successful_heartbeat(&conn, &config, &mut client);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.mutation_step -= 1;
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationStepMismatch { .. }
));
}
#[test]
fn test_repeated_simulation_keeps_server_and_client_commitments_equal() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let mut config = test_config();
config.gene_size = 128;
let (init, signing_key) = create_test_session(&conn, &config);
let mut client = client_from_init(&init, signing_key);
for _ in 0..10 {
run_successful_heartbeat(&conn, &config, &mut client);
let server_state = load_server_gene_state(&conn, &client.session_id);
assert_eq!(
gene::commitment(&server_state),
gene::commitment(&client.committed_gene_state)
);
}
}
}
+46 -1
View File
@@ -38,9 +38,54 @@ fn init_schema(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
chain_length INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
last_seen INTEGER NOT NULL,
expires_at INTEGER NOT NULL
expires_at INTEGER NOT NULL,
gene BLOB NOT NULL DEFAULT X'',
environment BLOB NOT NULL DEFAULT X'',
pending_mutation BLOB NOT NULL DEFAULT X'',
pending_mutation_step INTEGER NOT NULL DEFAULT 0
);",
)?;
ensure_column(
conn,
"gene",
"ALTER TABLE sessions ADD COLUMN gene BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"environment",
"ALTER TABLE sessions ADD COLUMN environment BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"pending_mutation",
"ALTER TABLE sessions ADD COLUMN pending_mutation BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"pending_mutation_step",
"ALTER TABLE sessions ADD COLUMN pending_mutation_step INTEGER NOT NULL DEFAULT 0",
)?;
conn.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);",
)?;
Ok(())
}
fn ensure_column(
conn: &rusqlite::Connection,
column: &str,
alter_sql: &str,
) -> Result<(), rusqlite::Error> {
let exists: bool = conn.query_row(
"SELECT EXISTS(
SELECT 1 FROM pragma_table_info('sessions') WHERE name = ?1
)",
[column],
|row| row.get(0),
)?;
if !exists {
conn.execute_batch(alter_sql)?;
}
Ok(())
}
+53 -3
View File
@@ -1,4 +1,8 @@
use rand::Rng;
use shared::{
gene::GeneState,
vm_extensions::{self, ExecutionTrace, MutationError, MutationOrder},
};
pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Vec<u8> {
let mut rng = rand::thread_rng();
@@ -9,7 +13,7 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
if depth < 2 {
// Not enough operands for any binary op — push a literal.
ops.push(0x00);
let val = rng.gen::<u32>();
let val = rng.r#gen::<u32>();
ops.extend_from_slice(&val.to_le_bytes());
depth += 1;
} else {
@@ -18,7 +22,7 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
0x00 => {
// PUSH literal
ops.push(0x00);
let val = rng.gen::<u32>();
let val = rng.r#gen::<u32>();
ops.extend_from_slice(&val.to_le_bytes());
depth += 1;
}
@@ -43,4 +47,50 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
ops
}
// Server does not need to execute the program; client does.
pub fn execute_mutation_program(
state: &mut GeneState,
program: &[u8],
) -> Result<ExecutionTrace, MutationError> {
vm_extensions::execute_program(state, program)
}
pub fn execute_mutation_order(
state: &mut GeneState,
order: &MutationOrder,
) -> Result<ExecutionTrace, MutationError> {
vm_extensions::execute_program(state, &order.program)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use shared::gene::{commitment, new_state};
#[test]
fn test_execute_mutation_program_wraps_shared_engine() {
let mut state = new_state(8).unwrap();
let program = vec![vm_extensions::OP_MUTATE_POINT, 0, 0, 1];
let trace = execute_mutation_program(&mut state, &program).unwrap();
assert_eq!(state.gene[0], 1);
assert_eq!(trace.final_ip, program.len());
}
#[test]
fn test_execute_mutation_order_determinism() {
let mut rng_a = rand::rngs::StdRng::seed_from_u64(101);
let mut rng_b = rand::rngs::StdRng::seed_from_u64(101);
let order_a = vm_extensions::generate_order_with_rng(&mut rng_a, 9, 64);
let order_b = vm_extensions::generate_order_with_rng(&mut rng_b, 9, 64);
assert_eq!(order_a, order_b);
let mut state_a = new_state(64).unwrap();
let mut state_b = new_state(64).unwrap();
let trace_a = execute_mutation_order(&mut state_a, &order_a).unwrap();
let trace_b = execute_mutation_order(&mut state_b, &order_b).unwrap();
assert_eq!(state_a, state_b);
assert_eq!(trace_a.final_stack, trace_b.final_stack);
assert_eq!(commitment(&state_a), commitment(&state_b));
}
}