#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::path::PathBuf;
use chio_core::canonical::canonical_json_bytes;
use chio_core::crypto::{sha256_hex, Keypair};
use chio_core::merkle::MerkleTree;
use chio_core::receipt::{
body::chio_receipt_id, body::ChioReceipt, body::ChioReceiptBody, decision::Decision,
decision::ToolCallAction, kinds::TrustLevel, signing::CHIO_RECEIPT_SIGNING_NONCE_METADATA_KEY,
};
use proptest::prelude::*;
use proptest::test_runner::{Config as ProptestConfig, FileFailurePersistence};
use serde_json::json;
const KERNEL_SEED: [u8; 32] = [
0xC4, 0x10, 0x73, 0x29, 0xA6, 0xB2, 0x4D, 0x5F, 0x91, 0x08, 0x42, 0x6E, 0xD7, 0xCB, 0x33, 0x80,
0x1E, 0x55, 0xAA, 0x77, 0x16, 0x9C, 0x3B, 0xE0, 0x4F, 0x82, 0x69, 0x12, 0xBD, 0x05, 0x2A, 0xCC,
];
const SIGNING_FUNCTION_CASES: u32 = 256;
const REPLAY_ROOT_CASES: u32 = 256;
const SHUFFLE_INDEPENDENCE_CASES: u32 = 256;
const SHUFFLE_INDEPENDENCE_MAX: usize = 16;
const REPLAY_RANDOM_MAX: usize = 32;
fn kernel_keypair() -> Keypair {
Keypair::from_seed(&KERNEL_SEED)
}
#[derive(Debug, Clone)]
struct ReceiptTuple {
decision: Decision,
payload: serde_json::Value,
clock: u64,
nonce: String,
}
fn arbitrary_decision() -> impl Strategy<Value = Decision> {
prop_oneof![
Just(Decision::Allow),
("[a-z]{3,16}", "[a-z]{3,16}").prop_map(|(reason, guard)| Decision::Deny { reason, guard }),
"[a-z]{3,16}".prop_map(|reason| Decision::Cancelled { reason }),
"[a-z]{3,16}".prop_map(|reason| Decision::Incomplete { reason }),
]
}
fn arbitrary_payload() -> impl Strategy<Value = serde_json::Value> {
(
"[a-z]{1,12}",
proptest::collection::vec(0u8..=255, 0..16),
any::<i32>(),
)
.prop_map(|(method, bytes, magnitude)| {
json!({
"method": method,
"bytes": bytes,
"magnitude": magnitude,
})
})
}
fn arbitrary_receipt_tuple() -> impl Strategy<Value = ReceiptTuple> {
(
arbitrary_decision(),
arbitrary_payload(),
any::<u64>(),
"[a-z0-9]{8,32}",
)
.prop_map(|(decision, payload, clock, nonce)| ReceiptTuple {
decision,
payload,
clock,
nonce,
})
}
fn edge_case_payload() -> impl Strategy<Value = serde_json::Value> {
prop_oneof![
Just(json!({})),
Just(json!({
"method": "z",
"bytes": vec![0u8; 16],
"magnitude": 0,
})),
Just(json!({
"method": "max",
"bytes": Vec::<u8>::new(),
"magnitude": i32::MAX,
})),
Just(json!({
"method": "min",
"bytes": Vec::<u8>::new(),
"magnitude": i32::MIN,
})),
Just(json!({
"method": "h",
"bytes": vec![0xFFu8],
"magnitude": 1,
})),
]
}
fn signing_edge_tuple() -> impl Strategy<Value = ReceiptTuple> {
prop_oneof![
6 => arbitrary_receipt_tuple(),
1 => (arbitrary_decision(), edge_case_payload(), Just(0u64), "[a-z]{1}").prop_map(
|(decision, payload, clock, nonce)| ReceiptTuple { decision, payload, clock, nonce },
),
1 => (
arbitrary_decision(),
edge_case_payload(),
Just(u64::MAX),
"[a-z0-9]{32}",
)
.prop_map(|(decision, payload, clock, nonce)| ReceiptTuple {
decision,
payload,
clock,
nonce,
}),
1 => (
arbitrary_decision(),
arbitrary_payload(),
prop_oneof![Just(1u64), Just(i64::MAX as u64), Just(u64::MAX - 1)],
"[a-z0-9]{8,32}",
)
.prop_map(|(decision, payload, clock, nonce)| ReceiptTuple {
decision,
payload,
clock,
nonce,
}),
1 => (arbitrary_decision(), edge_case_payload(), any::<u64>(), "[a-z0-9]{8,32}").prop_map(
|(decision, payload, clock, nonce)| ReceiptTuple { decision, payload, clock, nonce },
),
]
}
fn replay_sequence() -> impl Strategy<Value = Vec<ReceiptTuple>> {
prop_oneof![
1 => Just(Vec::<ReceiptTuple>::new()),
1 => proptest::collection::vec(arbitrary_receipt_tuple(), 1..=1),
1 => proptest::collection::vec(arbitrary_receipt_tuple(), 2..=2),
4 => proptest::collection::vec(arbitrary_receipt_tuple(), 3..=REPLAY_RANDOM_MAX),
]
.prop_filter("receipt nonces must be unique per batch", |ts| {
let mut seen = std::collections::HashSet::new();
ts.iter().all(|t| seen.insert(t.nonce.clone()))
})
}
fn body_from_tuple(tuple: &ReceiptTuple, kernel_key: &Keypair) -> ChioReceiptBody {
let action =
ToolCallAction::from_parameters(tuple.payload.clone()).expect("payload canonicalises");
let content_hash = sha256_hex(action.parameter_hash.as_bytes());
let policy_hash = sha256_hex(format!("policy:{}", tuple.nonce).as_bytes());
let mut body = ChioReceiptBody {
id: format!("rcpt-{}", tuple.nonce),
timestamp: tuple.clock,
capability_id: format!("cap-{}", tuple.nonce),
tool_server: "tool.example".to_string(),
tool_name: "echo".to_string(),
action,
decision: Some(tuple.decision.clone()),
receipt_kind: Default::default(),
boundary_class: Default::default(),
observation_outcome: None,
tool_origin: Default::default(),
redaction_mode: Default::default(),
actor_chain: Vec::new(),
content_hash,
policy_hash,
evidence: Vec::new(),
metadata: None,
trust_level: TrustLevel::default(),
tenant_id: None,
kernel_key: kernel_key.public_key(),
bbs_projection_version: None,
};
body.id = chio_receipt_id(&body).expect("canonical receipt id computes");
body
}
fn sign_body(body: &ChioReceiptBody, kernel_key: &Keypair) -> ChioReceipt {
ChioReceipt::sign(body.clone(), kernel_key).expect("signing succeeds")
}
fn canonical_body_bytes(body: &ChioReceiptBody) -> Vec<u8> {
canonical_json_bytes(body).expect("body canonicalises")
}
fn bind_signing_nonce(body: &mut ChioReceiptBody) {
let nonce = body.id.trim();
if nonce.is_empty() {
return;
}
let mut metadata = match body.metadata.take() {
Some(serde_json::Value::Object(map)) => map,
Some(value) => {
let mut map = serde_json::Map::new();
map.insert("original_metadata".to_string(), value);
map
}
None => serde_json::Map::new(),
};
metadata.insert(
CHIO_RECEIPT_SIGNING_NONCE_METADATA_KEY.to_string(),
serde_json::Value::String(nonce.to_string()),
);
body.metadata = Some(serde_json::Value::Object(metadata));
}
#[derive(Debug, PartialEq, Eq)]
enum AnchorOutcome {
Root([u8; 32]),
EmptyTree(String),
}
fn try_anchor_root(receipts: &[ChioReceipt]) -> AnchorOutcome {
let leaves: Vec<Vec<u8>> = receipts
.iter()
.map(|r| canonical_body_bytes(&r.body()))
.collect();
match MerkleTree::from_leaves(&leaves) {
Ok(tree) => AnchorOutcome::Root(*tree.root().as_bytes()),
Err(err) => AnchorOutcome::EmptyTree(err.to_string()),
}
}
fn regression_persistence() -> Box<FileFailurePersistence> {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.pop(); path.pop(); path.pop(); path.push("tests");
path.push("replay");
path.push("proptest-regressions");
path.push("replay_proptest.txt");
Box::new(FileFailurePersistence::Direct(Box::leak(
path.to_string_lossy().into_owned().into_boxed_str(),
)))
}
proptest! {
#![proptest_config(ProptestConfig {
cases: SIGNING_FUNCTION_CASES,
failure_persistence: Some(regression_persistence()),
.. ProptestConfig::default()
})]
#[test]
fn signing_is_a_function(tuple in signing_edge_tuple()) {
let kp = kernel_keypair();
let kp_twin = kernel_keypair();
prop_assert_eq!(kp.public_key(), kp_twin.public_key());
let body = body_from_tuple(&tuple, &kp);
let receipt_a = sign_body(&body, &kp);
let receipt_b = sign_body(&body, &kp);
let receipt_c = sign_body(&body, &kp_twin);
let bytes_a = canonical_json_bytes(&receipt_a).expect("receipt a canonicalises");
let bytes_b = canonical_json_bytes(&receipt_b).expect("receipt b canonicalises");
let bytes_c = canonical_json_bytes(&receipt_c).expect("receipt c canonicalises");
prop_assert_eq!(&bytes_a, &bytes_b);
prop_assert_eq!(&bytes_a, &bytes_c);
let sig_a = canonical_json_bytes(&receipt_a.signature)
.expect("signature a canonicalises");
let sig_b = canonical_json_bytes(&receipt_b.signature)
.expect("signature b canonicalises");
let sig_c = canonical_json_bytes(&receipt_c.signature)
.expect("signature c canonicalises");
prop_assert_eq!(&sig_a, &sig_b);
prop_assert_eq!(&sig_a, &sig_c);
prop_assert!(receipt_a.verify_signature().expect("verify a"));
prop_assert!(receipt_b.verify_signature().expect("verify b"));
prop_assert!(receipt_c.verify_signature().expect("verify c"));
let body_bytes_a = canonical_body_bytes(&receipt_a.body());
let body_bytes_b = canonical_body_bytes(&receipt_b.body());
let mut nonce_bound_body = body.clone();
bind_signing_nonce(&mut nonce_bound_body);
nonce_bound_body.id =
chio_receipt_id(&nonce_bound_body).expect("nonce-bound receipt id computes");
let body_bytes_direct = canonical_body_bytes(&nonce_bound_body);
prop_assert_eq!(&body_bytes_a, &body_bytes_b);
prop_assert_eq!(&body_bytes_a, &body_bytes_direct);
prop_assert!(
receipt_a
.body()
.action
.verify_hash()
.expect("parameter hash verifies")
);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: REPLAY_ROOT_CASES,
failure_persistence: Some(regression_persistence()),
.. ProptestConfig::default()
})]
#[test]
fn replay_root_is_idempotent(tuples in replay_sequence()) {
let kp = kernel_keypair();
let receipts_a: Vec<ChioReceipt> = tuples
.iter()
.map(|t| sign_body(&body_from_tuple(t, &kp), &kp))
.collect();
let receipts_b: Vec<ChioReceipt> = tuples
.iter()
.map(|t| sign_body(&body_from_tuple(t, &kp), &kp))
.collect();
let outcome_a = try_anchor_root(&receipts_a);
let outcome_b = try_anchor_root(&receipts_b);
prop_assert_eq!(&outcome_a, &outcome_b);
match (&outcome_a, tuples.is_empty()) {
(AnchorOutcome::EmptyTree(_), true) => {}
(AnchorOutcome::Root(_), false) => {}
(AnchorOutcome::EmptyTree(_), false) => {
prop_assert!(
false,
"non-empty receipt batch produced EmptyTree outcome"
);
}
(AnchorOutcome::Root(_), true) => {
prop_assert!(
false,
"empty receipt batch produced a Merkle root"
);
}
}
}
}
fn independent_tuple_batch() -> impl Strategy<Value = Vec<ReceiptTuple>> {
proptest::collection::vec(arbitrary_receipt_tuple(), 2..=SHUFFLE_INDEPENDENCE_MAX)
.prop_filter("receipt nonces must be unique per batch", |ts| {
let mut seen = std::collections::HashSet::new();
ts.iter().all(|t| seen.insert(t.nonce.clone()))
})
.prop_map(|ts| {
ts.into_iter()
.map(|mut t| {
if let Some(obj) = t.payload.as_object_mut() {
obj.insert(
"method".to_string(),
serde_json::Value::String(format!("m-{}", t.nonce)),
);
} else {
t.payload = json!({
"method": format!("m-{}", t.nonce),
"bytes": Vec::<u8>::new(),
"magnitude": 0,
});
}
t
})
.collect()
})
}
proptest! {
#![proptest_config(ProptestConfig {
cases: SHUFFLE_INDEPENDENCE_CASES,
failure_persistence: Some(regression_persistence()),
.. ProptestConfig::default()
})]
#[test]
fn shuffle_independent_receipts_preserves_bytes(
tuples in independent_tuple_batch(),
seed in any::<u64>(),
) {
let kp = kernel_keypair();
let receipts: Vec<ChioReceipt> = tuples
.iter()
.map(|t| sign_body(&body_from_tuple(t, &kp), &kp))
.collect();
let baseline_bytes: Vec<Vec<u8>> = receipts
.iter()
.map(|r| canonical_body_bytes(&r.body()))
.collect();
let nonces: std::collections::HashSet<&str> =
tuples.iter().map(|t| t.nonce.as_str()).collect();
prop_assert_eq!(nonces.len(), tuples.len());
let content_hashes: std::collections::HashSet<String> = receipts
.iter()
.map(|r| r.body().content_hash.clone())
.collect();
prop_assert_eq!(content_hashes.len(), receipts.len());
let baseline_set: std::collections::HashSet<Vec<u8>> =
baseline_bytes.iter().cloned().collect();
prop_assert_eq!(baseline_set.len(), baseline_bytes.len());
let mut indices: Vec<usize> = (0..receipts.len()).collect();
let mut state = seed | 1;
for i in (1..indices.len()).rev() {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let j = (state as usize) % (i + 1);
indices.swap(i, j);
}
let shuffled: Vec<ChioReceipt> =
indices.iter().map(|&i| receipts[i].clone()).collect();
let shuffled_bytes: Vec<Vec<u8>> = shuffled
.iter()
.map(|r| canonical_body_bytes(&r.body()))
.collect();
for (j, &original_index) in indices.iter().enumerate() {
prop_assert_eq!(
&shuffled_bytes[j],
&baseline_bytes[original_index],
);
}
let mut baseline_sorted = baseline_bytes.clone();
baseline_sorted.sort();
let mut shuffled_sorted = shuffled_bytes.clone();
shuffled_sorted.sort();
prop_assert_eq!(baseline_sorted, shuffled_sorted);
}
}