From e4bfd1419ca5b4fdbde5059bde8588c7e2a1eafd Mon Sep 17 00:00:00 2001 From: mehdi-defiesta Date: Wed, 21 Jan 2026 18:53:49 +0100 Subject: [PATCH 1/3] Add consensus-critical fields to kernel types for on-chain verifiability - KernelInputV1: Add kernel_version, agent_code_hash, constraint_set_hash, input_root, execution_nonce for binding proofs to code/policy/state - KernelJournalV1: Add identity fields (agent_id, agent_code_hash, constraint_set_hash, input_root, execution_nonce) for verifier binding - Replace opaque AgentOutput with structured ActionV1 format (action_type, target, payload) with deterministic ordering - Introduce KernelError, AgentError, ConstraintError type hierarchy - Update Agent trait with AgentContext containing full execution context - Update codec with explicit encoding layouts and size documentation - Expand test suite to 24 tests covering new fields and edge cases --- crates/agent-traits/src/lib.rs | 119 ++++++++-- crates/constraints/src/lib.rs | 77 ++++++- crates/host-tests/src/lib.rs | 366 ++++++++++++++++++++----------- crates/kernel-core/src/codec.rs | 377 ++++++++++++++++++++++++++------ crates/kernel-core/src/lib.rs | 11 +- crates/kernel-core/src/types.rs | 143 +++++++++++- crates/kernel-guest/src/lib.rs | 121 ++++++---- 7 files changed, 941 insertions(+), 273 deletions(-) diff --git a/crates/agent-traits/src/lib.rs b/crates/agent-traits/src/lib.rs index 49b9b0e..49340f5 100644 --- a/crates/agent-traits/src/lib.rs +++ b/crates/agent-traits/src/lib.rs @@ -1,30 +1,109 @@ -use kernel_core::{AgentOutput, MAX_AGENT_OUTPUT_BYTES}; +use kernel_core::{AgentOutput, ActionV1, AgentError, MAX_ACTIONS_PER_OUTPUT}; -#[derive(Clone, Debug, PartialEq)] -pub enum AgentError { - InvalidInput, - ExecutionFailed, - OutputTooLarge, +/// Context provided to agents during execution. +/// +/// Contains all identity and state information the agent needs +/// to make decisions and produce actions. +#[derive(Clone, Debug)] +pub struct AgentContext { + /// 32-byte agent identifier + pub agent_id: [u8; 32], + /// SHA-256 hash of the agent's own code + pub agent_code_hash: [u8; 32], + /// SHA-256 hash of the constraint set being enforced + pub constraint_set_hash: [u8; 32], + /// External state root (market/vault snapshot) + pub input_root: [u8; 32], + /// Execution nonce for replay protection + pub execution_nonce: u64, } +/// Canonical agent interface. +/// +/// All agents must implement this trait. The kernel calls `run()` +/// with the execution context and opaque inputs, and expects +/// a structured `AgentOutput` containing ordered actions. +/// +/// # Determinism Requirements +/// +/// Implementations MUST be fully deterministic: +/// - No randomness or time dependencies +/// - No floating-point operations +/// - No unordered iteration +/// - Bounded loops and memory usage pub trait Agent { - fn run(input_root: [u8; 32], inputs: &[u8]) -> Result; + /// Execute the agent with the given context and inputs. + /// + /// # Arguments + /// * `ctx` - Execution context with identity and state information + /// * `inputs` - Opaque agent-specific input data + /// + /// # Returns + /// * `Ok(AgentOutput)` - Structured output containing ordered actions + /// * `Err(AgentError)` - Execution failed, kernel will abort + fn run(ctx: &AgentContext, inputs: &[u8]) -> Result; } +/// Trivial reference agent implementation. +/// +/// WARNING: This is NOT production-ready and exists only for testing. +/// It converts input bytes into a single "echo" action. pub struct TrivialAgent; +/// Action type for echo action (used by TrivialAgent) +pub const ACTION_TYPE_ECHO: u32 = 0x00000001; + impl Agent for TrivialAgent { - /// WARNING: TrivialAgent is NOT production-ready and should only be used for testing. - /// It simply echoes input as output without any validation or processing. - fn run(_input_root: [u8; 32], inputs: &[u8]) -> Result { - if inputs.len() > MAX_AGENT_OUTPUT_BYTES { - return Err(AgentError::OutputTooLarge); - } - - // Pre-allocate with known size for efficiency - let mut data = Vec::with_capacity(inputs.len()); - data.extend_from_slice(inputs); - - Ok(AgentOutput { data }) + /// Converts input into a single echo action. + /// + /// The action targets the agent's own ID and carries + /// the input as payload (truncated to max payload size). + fn run(ctx: &AgentContext, inputs: &[u8]) -> Result { + // Truncate to max payload size if needed + let payload_len = inputs.len().min(kernel_core::MAX_ACTION_PAYLOAD_BYTES); + let payload = inputs[..payload_len].to_vec(); + + let action = ActionV1 { + action_type: ACTION_TYPE_ECHO, + target: ctx.agent_id, + payload, + }; + + Ok(AgentOutput { + actions: vec![action], + }) + } +} + +/// No-op agent that produces no actions. +/// +/// Useful for testing constraint enforcement with empty output. +pub struct NoOpAgent; + +impl Agent for NoOpAgent { + fn run(_ctx: &AgentContext, _inputs: &[u8]) -> Result { + Ok(AgentOutput { actions: vec![] }) } -} \ No newline at end of file +} + +/// Agent that produces multiple actions for testing. +pub struct MultiActionAgent; + +impl Agent for MultiActionAgent { + /// Produces one action per byte of input (up to MAX_ACTIONS_PER_OUTPUT). + fn run(ctx: &AgentContext, inputs: &[u8]) -> Result { + let action_count = inputs.len().min(MAX_ACTIONS_PER_OUTPUT); + + let actions: Vec = inputs[..action_count] + .iter() + .enumerate() + .map(|(i, &byte)| ActionV1 { + action_type: i as u32, + target: ctx.agent_id, + payload: vec![byte], + }) + .collect(); + + Ok(AgentOutput { actions }) + } +} diff --git a/crates/constraints/src/lib.rs b/crates/constraints/src/lib.rs index ea9f615..b646fe6 100644 --- a/crates/constraints/src/lib.rs +++ b/crates/constraints/src/lib.rs @@ -1,16 +1,73 @@ -use kernel_core::AgentOutput; - -#[derive(Clone, Debug, PartialEq)] -pub enum ConstraintError { - ViolatedConstraint, - InvalidOutput, -} +use kernel_core::{AgentOutput, ConstraintError}; +/// Metadata for constraint checking. +/// +/// Contains context information needed to evaluate constraints +/// against the agent's output. #[derive(Clone, Debug)] -pub struct Meta { +pub struct ConstraintMeta { + /// 32-byte agent identifier pub agent_id: [u8; 32], + /// SHA-256 hash of the agent code + pub agent_code_hash: [u8; 32], + /// SHA-256 hash of the constraint set being enforced + pub constraint_set_hash: [u8; 32], + /// External state root that was observed + pub input_root: [u8; 32], + /// Execution nonce + pub execution_nonce: u64, } -pub fn check(_output: &AgentOutput, _meta: &Meta) -> Result<(), ConstraintError> { +/// Check agent output against constraint set. +/// +/// This function is MANDATORY - the kernel MUST call it and +/// MUST abort if it returns an error. +/// +/// # P0.1 Implementation +/// +/// For P0.1, this is a stub that always returns Ok(()). +/// Full constraint enforcement will be implemented in P0.2+. +/// +/// # Future Implementation +/// +/// Will validate: +/// - Action types are allowed for this agent +/// - Targets are within permitted scope +/// - Payload values are within bounds +/// - Total value/risk is within limits +/// - Rate limits are respected +/// +/// # Arguments +/// * `output` - The agent's structured output +/// * `meta` - Constraint checking metadata +/// +/// # Returns +/// * `Ok(())` - All constraints satisfied +/// * `Err(ConstraintError)` - A constraint was violated +pub fn check(_output: &AgentOutput, _meta: &ConstraintMeta) -> Result<(), ConstraintError> { + // P0.1: Stub implementation - always passes + // P0.2+: Will implement actual constraint checking Ok(()) -} \ No newline at end of file +} + +/// Validate that output is well-formed before constraint checking. +/// +/// Ensures basic structural validity: +/// - Action count within limits +/// - Payload sizes within limits +/// - Required fields present +pub fn validate_output_structure(output: &AgentOutput) -> Result<(), ConstraintError> { + use kernel_core::{MAX_ACTIONS_PER_OUTPUT, MAX_ACTION_PAYLOAD_BYTES}; + + if output.actions.len() > MAX_ACTIONS_PER_OUTPUT { + return Err(ConstraintError::InvalidOutput); + } + + for action in &output.actions { + if action.payload.len() > MAX_ACTION_PAYLOAD_BYTES { + return Err(ConstraintError::InvalidOutput); + } + } + + Ok(()) +} diff --git a/crates/host-tests/src/lib.rs b/crates/host-tests/src/lib.rs index b8cd377..085732e 100644 --- a/crates/host-tests/src/lib.rs +++ b/crates/host-tests/src/lib.rs @@ -3,13 +3,23 @@ mod tests { use kernel_core::*; use kernel_guest::kernel_main; + /// Helper to create a valid KernelInputV1 with default values + fn make_input(agent_input: Vec) -> KernelInputV1 { + KernelInputV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0x42; 32], + agent_code_hash: [0xaa; 32], + constraint_set_hash: [0xbb; 32], + input_root: [0xcc; 32], + execution_nonce: 1, + opaque_agent_inputs: agent_input, + } + } + #[test] fn test_kernel_input_encoding_round_trip() { - let original = KernelInputV1 { - protocol_version: 1, - agent_id: [0x42; 32], - agent_input: vec![1, 2, 3, 4, 5], - }; + let original = make_input(vec![1, 2, 3, 4, 5]); let encoded = original.encode(); let decoded = KernelInputV1::decode(&encoded).unwrap(); @@ -20,10 +30,15 @@ mod tests { #[test] fn test_kernel_journal_encoding_round_trip() { let original = KernelJournalV1 { - protocol_version: 1, - kernel_version: 1, - input_commitment: [0xaa; 32], - action_commitment: [0xbb; 32], + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0x42; 32], + agent_code_hash: [0xaa; 32], + constraint_set_hash: [0xbb; 32], + input_root: [0xcc; 32], + execution_nonce: 12345, + input_commitment: [0xdd; 32], + action_commitment: [0xee; 32], execution_status: ExecutionStatus::Success, }; @@ -33,10 +48,35 @@ mod tests { assert_eq!(original, decoded); } + #[test] + fn test_action_encoding_round_trip() { + let original = ActionV1 { + action_type: 0x12345678, + target: [0x99; 32], + payload: vec![10, 20, 30, 40], + }; + + let encoded = original.encode(); + let decoded = ActionV1::decode(&encoded).unwrap(); + + assert_eq!(original, decoded); + } + #[test] fn test_agent_output_encoding_round_trip() { let original = AgentOutput { - data: vec![10, 20, 30, 40], + actions: vec![ + ActionV1 { + action_type: 1, + target: [0x11; 32], + payload: vec![1, 2, 3], + }, + ActionV1 { + action_type: 2, + target: [0x22; 32], + payload: vec![4, 5, 6, 7, 8], + }, + ], }; let encoded = original.encode(); @@ -45,47 +85,57 @@ mod tests { assert_eq!(original, decoded); } + #[test] + fn test_empty_agent_output_encoding() { + let original = AgentOutput { actions: vec![] }; + + let encoded = original.encode(); + let decoded = AgentOutput::decode(&encoded).unwrap(); + + assert_eq!(original, decoded); + assert_eq!(encoded.len(), 4); // Just the count field + } + #[test] fn test_input_commitment_golden_vector() { + // Using simple input bytes for reproducible test let input_bytes = vec![1, 2, 3, 4]; let commitment = compute_input_commitment(&input_bytes); - + + // SHA256([1,2,3,4]) let expected = [ 0x9f, 0x64, 0xa7, 0x47, 0xe1, 0xb9, 0x7f, 0x13, 0x1f, 0xab, 0xb6, 0xb4, 0x47, 0x29, 0x6c, 0x9b, 0x6f, 0x02, 0x01, 0xe7, 0x9f, 0xb3, 0xc5, 0x35, 0x6e, 0x6c, 0x77, 0xe8, 0x9b, 0x6a, 0x80, 0x6a ]; - + assert_eq!(commitment, expected); } #[test] fn test_action_commitment_golden_vector() { - let agent_output = AgentOutput { data: vec![5, 6, 7, 8] }; + // Empty actions list encodes to [0, 0, 0, 0] (count = 0) + let agent_output = AgentOutput { actions: vec![] }; let output_bytes = agent_output.encode(); let commitment = compute_action_commitment(&output_bytes); - + + // SHA256([0, 0, 0, 0]) - empty action list let expected = [ - 0xf4, 0xef, 0xd2, 0x8a, 0x94, 0x8f, 0x87, 0xf5, - 0x31, 0x86, 0x97, 0x5e, 0xe0, 0x8f, 0xad, 0x42, - 0x57, 0x9e, 0x8d, 0x8d, 0xad, 0x9c, 0x2a, 0x98, - 0xd1, 0x8c, 0xa3, 0x5a, 0x68, 0xb8, 0x3e, 0x76 + 0xdf, 0x3f, 0x61, 0x98, 0x04, 0xa9, 0x2f, 0xdb, + 0x40, 0x57, 0x19, 0x2d, 0xc4, 0x3d, 0xd7, 0x48, + 0xea, 0x77, 0x8a, 0xdc, 0x52, 0xbc, 0x49, 0x8c, + 0xe8, 0x05, 0x24, 0xc0, 0x14, 0xb8, 0x11, 0x19 ]; - + assert_eq!(commitment, expected); } #[test] fn test_determinism() { - let input = KernelInputV1 { - protocol_version: 1, - agent_id: [0x12; 32], - agent_input: vec![100, 200], - }; - + let input = make_input(vec![100, 200]); let input_bytes = input.encode(); - + let result1 = kernel_main(&input_bytes).unwrap(); let result2 = kernel_main(&input_bytes).unwrap(); @@ -94,30 +144,34 @@ mod tests { #[test] fn test_invalid_protocol_version() { - let input = KernelInputV1 { - protocol_version: 999, - agent_id: [0x12; 32], - agent_input: vec![1, 2, 3], - }; + let mut input = make_input(vec![1, 2, 3]); + input.protocol_version = 999; + + let input_bytes = input.encode(); + let result = KernelInputV1::decode(&input_bytes); + + assert!(matches!(result, Err(CodecError::InvalidVersion { expected: 1, actual: 999 }))); + } + + #[test] + fn test_invalid_kernel_version() { + let mut input = make_input(vec![1, 2, 3]); + input.kernel_version = 999; let input_bytes = input.encode(); let result = KernelInputV1::decode(&input_bytes); - - assert!(matches!(result, Err(CodecError::InvalidVersion { .. }))); + + assert!(matches!(result, Err(CodecError::InvalidVersion { expected: 1, actual: 999 }))); } #[test] fn test_input_too_large() { let large_input = vec![0u8; MAX_AGENT_INPUT_BYTES + 1]; - let input = KernelInputV1 { - protocol_version: 1, - agent_id: [0x12; 32], - agent_input: large_input, - }; + let input = make_input(large_input); let input_bytes = input.encode(); let result = KernelInputV1::decode(&input_bytes); - + assert!(matches!(result, Err(CodecError::InputTooLarge { .. }))); } @@ -125,67 +179,50 @@ mod tests { fn test_malformed_input() { let malformed = vec![1, 2, 3]; let result = KernelInputV1::decode(&malformed); - + assert!(matches!(result, Err(CodecError::UnexpectedEndOfInput))); } #[test] fn test_journal_fixed_size() { let journal = KernelJournalV1 { - protocol_version: 1, - kernel_version: 1, + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0; 32], + agent_code_hash: [0; 32], + constraint_set_hash: [0; 32], + input_root: [0; 32], + execution_nonce: 0, input_commitment: [0; 32], action_commitment: [0; 32], execution_status: ExecutionStatus::Success, }; let encoded = journal.encode(); - assert_eq!(encoded.len(), 73); + // protocol_version: 4 + kernel_version: 4 + agent_id: 32 + + // agent_code_hash: 32 + constraint_set_hash: 32 + input_root: 32 + + // execution_nonce: 8 + input_commitment: 32 + action_commitment: 32 + + // execution_status: 1 = 209 bytes + assert_eq!(encoded.len(), 209); } #[test] fn test_constraints_enforcement() { - let input = KernelInputV1 { - protocol_version: 1, - agent_id: [0x99; 32], - agent_input: vec![1, 2, 3], - }; - + let input = make_input(vec![1, 2, 3]); let input_bytes = input.encode(); let result = kernel_main(&input_bytes); - - assert!(result.is_ok()); - } - - #[test] - fn test_unsupported_protocol_version_error() { - use kernel_guest::{kernel_main, KernelError}; - - let input = KernelInputV1 { - protocol_version: 999, - agent_id: [0x12; 32], - agent_input: vec![1, 2, 3], - }; - let input_bytes = input.encode(); - let result = kernel_main(&input_bytes); - - assert!(matches!(result, Err(KernelError::InvalidInput(CodecError::InvalidVersion { .. })))); + assert!(result.is_ok()); } #[test] fn test_empty_input() { - let input = KernelInputV1 { - protocol_version: 1, - agent_id: [0xaa; 32], - agent_input: vec![], - }; - + let input = make_input(vec![]); let input_bytes = input.encode(); let result = kernel_main(&input_bytes); - + assert!(result.is_ok()); - + let journal_bytes = result.unwrap(); let journal = KernelJournalV1::decode(&journal_bytes).unwrap(); assert_eq!(journal.execution_status, ExecutionStatus::Success); @@ -193,98 +230,165 @@ mod tests { #[test] fn test_max_size_input() { - let input = KernelInputV1 { - protocol_version: 1, - agent_id: [0xbb; 32], - agent_input: vec![0x42; MAX_AGENT_INPUT_BYTES], - }; - + let input = make_input(vec![0x42; MAX_AGENT_INPUT_BYTES]); let input_bytes = input.encode(); let result = kernel_main(&input_bytes); - + assert!(result.is_ok()); } - #[test] - fn test_oversized_agent_output() { - let large_output = AgentOutput { - data: vec![0x99; MAX_AGENT_OUTPUT_BYTES + 1], + #[test] + fn test_journal_contains_identity_fields() { + let input = KernelInputV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0x11; 32], + agent_code_hash: [0x22; 32], + constraint_set_hash: [0x33; 32], + input_root: [0x44; 32], + execution_nonce: 9999, + opaque_agent_inputs: vec![1, 2, 3], }; - - // This should panic in encoding due to size limit - std::panic::catch_unwind(|| { - large_output.encode(); - }).expect_err("Expected panic for oversized output"); + + let input_bytes = input.encode(); + let journal_bytes = kernel_main(&input_bytes).unwrap(); + let journal = KernelJournalV1::decode(&journal_bytes).unwrap(); + + // Verify identity fields are copied to journal + assert_eq!(journal.agent_id, [0x11; 32]); + assert_eq!(journal.agent_code_hash, [0x22; 32]); + assert_eq!(journal.constraint_set_hash, [0x33; 32]); + assert_eq!(journal.input_root, [0x44; 32]); + assert_eq!(journal.execution_nonce, 9999); } #[test] - fn test_agent_output_decode_too_large() { - // Manually create bytes that would decode to oversized output + fn test_too_many_actions() { + // Create bytes that would decode to too many actions let mut bytes = Vec::new(); - bytes.extend_from_slice(&((MAX_AGENT_OUTPUT_BYTES + 1) as u32).to_le_bytes()); - bytes.extend(vec![0x55; MAX_AGENT_OUTPUT_BYTES + 1]); - + bytes.extend_from_slice(&((MAX_ACTIONS_PER_OUTPUT + 1) as u32).to_le_bytes()); + let result = AgentOutput::decode(&bytes); - assert!(matches!(result, Err(CodecError::OutputTooLarge { .. }))); + assert!(matches!(result, Err(CodecError::TooManyActions { .. }))); } #[test] - fn test_arithmetic_overflow_protection() { - // Test u32 to usize conversion bounds checking is working - let input = KernelInputV1 { - protocol_version: 1, - agent_id: [0xcc; 32], - agent_input: vec![1, 2, 3], + fn test_action_payload_too_large() { + let mut bytes = Vec::new(); + // action_type + bytes.extend_from_slice(&1u32.to_le_bytes()); + // target + bytes.extend_from_slice(&[0u8; 32]); + // payload_len (too large) + bytes.extend_from_slice(&((MAX_ACTION_PAYLOAD_BYTES + 1) as u32).to_le_bytes()); + // We don't need actual payload data, decode will fail on length check + + let result = ActionV1::decode(&bytes); + assert!(matches!(result, Err(CodecError::ActionPayloadTooLarge { .. }))); + } + + #[test] + fn test_execution_status_encoding() { + // Success encodes as 0x00 + let journal = KernelJournalV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0; 32], + agent_code_hash: [0; 32], + constraint_set_hash: [0; 32], + input_root: [0; 32], + execution_nonce: 0, + input_commitment: [0; 32], + action_commitment: [0; 32], + execution_status: ExecutionStatus::Success, }; - let input_bytes = input.encode(); - - // Verify normal case works - assert!(KernelInputV1::decode(&input_bytes).is_ok()); + let encoded = journal.encode(); + // Last byte should be 0x00 for Success + assert_eq!(*encoded.last().unwrap(), 0x00); } #[test] - fn test_memory_efficient_allocations() { - // Test that we're using pre-sized allocations - let large_input = vec![0x77; 1000]; - let input = KernelInputV1 { - protocol_version: 1, - agent_id: [0xdd; 32], - agent_input: large_input, + fn test_invalid_execution_status_decode() { + let journal = KernelJournalV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0; 32], + agent_code_hash: [0; 32], + constraint_set_hash: [0; 32], + input_root: [0; 32], + execution_nonce: 0, + input_commitment: [0; 32], + action_commitment: [0; 32], + execution_status: ExecutionStatus::Success, }; - let encoded = input.encode(); - let decoded = KernelInputV1::decode(&encoded).unwrap(); - - assert_eq!(decoded.agent_input.len(), 1000); - assert_eq!(decoded.agent_input[0], 0x77); + let mut encoded = journal.encode(); + // Corrupt the status byte to an invalid value + *encoded.last_mut().unwrap() = 0xFF; + + let result = KernelJournalV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidExecutionStatus(0xFF)))); } #[test] fn test_determinism_with_edge_cases() { let test_cases = vec![ vec![], // Empty - vec![0], // Single byte - vec![0xFF; 100], // Repeated bytes - (0..255).collect::>(), // Sequential bytes + vec![0], // Single byte + vec![0xFF; 100], // Repeated bytes + (0..255).collect::>(), // Sequential bytes ]; for test_input in test_cases { - let input = KernelInputV1 { - protocol_version: 1, - agent_id: [0xee; 32], - agent_input: test_input, - }; - + let input = make_input(test_input); let input_bytes = input.encode(); - + // Run multiple times to ensure determinism let result1 = kernel_main(&input_bytes).unwrap(); - let result2 = kernel_main(&input_bytes).unwrap(); + let result2 = kernel_main(&input_bytes).unwrap(); let result3 = kernel_main(&input_bytes).unwrap(); - + assert_eq!(result1, result2); assert_eq!(result2, result3); } } -} \ No newline at end of file + + #[test] + fn test_nonce_in_journal() { + let input1 = KernelInputV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0x42; 32], + agent_code_hash: [0xaa; 32], + constraint_set_hash: [0xbb; 32], + input_root: [0xcc; 32], + execution_nonce: 1, + opaque_agent_inputs: vec![1, 2, 3], + }; + + let input2 = KernelInputV1 { + execution_nonce: 2, + ..input1.clone() + }; + + let journal1 = KernelJournalV1::decode(&kernel_main(&input1.encode()).unwrap()).unwrap(); + let journal2 = KernelJournalV1::decode(&kernel_main(&input2.encode()).unwrap()).unwrap(); + + assert_eq!(journal1.execution_nonce, 1); + assert_eq!(journal2.execution_nonce, 2); + + // Different nonces should produce different input commitments + assert_ne!(journal1.input_commitment, journal2.input_commitment); + } + + #[test] + fn test_input_header_size() { + // Verify minimum input size with empty data + let input = make_input(vec![]); + let encoded = input.encode(); + + // Fixed fields (144) + length prefix (4) + 0 bytes data = 148 + assert_eq!(encoded.len(), 148); + } +} diff --git a/crates/kernel-core/src/codec.rs b/crates/kernel-core/src/codec.rs index ddeffdc..0cd8549 100644 --- a/crates/kernel-core/src/codec.rs +++ b/crates/kernel-core/src/codec.rs @@ -1,5 +1,5 @@ use crate::types::*; -use crate::{MAX_AGENT_INPUT_BYTES, MAX_AGENT_OUTPUT_BYTES, PROTOCOL_VERSION}; +use crate::{MAX_AGENT_INPUT_BYTES, PROTOCOL_VERSION, KERNEL_VERSION}; pub trait CanonicalEncode { fn encode(&self) -> Vec; @@ -9,151 +9,268 @@ pub trait CanonicalDecode: Sized { fn decode(bytes: &[u8]) -> Result; } +/// KernelInputV1 encoding layout (little-endian): +/// - protocol_version: u32 (4 bytes) +/// - kernel_version: u32 (4 bytes) +/// - agent_id: [u8; 32] (32 bytes) +/// - agent_code_hash: [u8; 32] (32 bytes) +/// - constraint_set_hash: [u8; 32] (32 bytes) +/// - input_root: [u8; 32] (32 bytes) +/// - execution_nonce: u64 (8 bytes) +/// - opaque_agent_inputs_len: u32 (4 bytes) +/// - opaque_agent_inputs: [u8; len] (variable) +/// +/// Fixed header: 144 bytes + 4 byte length prefix + variable input data +/// Minimum size with empty input: 148 bytes impl CanonicalEncode for KernelInputV1 { fn encode(&self) -> Vec { - let data_len = self.agent_input.len(); + let data_len = self.opaque_agent_inputs.len(); if data_len > u32::MAX as usize { panic!("Input data too large for u32 length prefix"); } - - let total_len = 4 + 32 + 4 + data_len; + + // Fixed fields (144) + length prefix (4) + data + let total_len = 144 + 4 + data_len; let mut buf = Vec::with_capacity(total_len); - + buf.extend_from_slice(&self.protocol_version.to_le_bytes()); + buf.extend_from_slice(&self.kernel_version.to_le_bytes()); buf.extend_from_slice(&self.agent_id); + buf.extend_from_slice(&self.agent_code_hash); + buf.extend_from_slice(&self.constraint_set_hash); + buf.extend_from_slice(&self.input_root); + buf.extend_from_slice(&self.execution_nonce.to_le_bytes()); buf.extend_from_slice(&(data_len as u32).to_le_bytes()); - buf.extend_from_slice(&self.agent_input); - + buf.extend_from_slice(&self.opaque_agent_inputs); + buf } } impl CanonicalDecode for KernelInputV1 { fn decode(bytes: &[u8]) -> Result { - if bytes.len() < 4 { + // Minimum size: fixed fields (144) + length prefix (4) = 148 bytes + if bytes.len() < 148 { return Err(CodecError::UnexpectedEndOfInput); } - + let mut offset = 0; - + + // protocol_version let protocol_version = u32::from_le_bytes( bytes[offset..offset + 4] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)? ); offset += 4; - + if protocol_version != PROTOCOL_VERSION { return Err(CodecError::InvalidVersion { expected: PROTOCOL_VERSION, actual: protocol_version, }); } - - if bytes.len() < offset + 32 { - return Err(CodecError::UnexpectedEndOfInput); + + // kernel_version + let kernel_version = u32::from_le_bytes( + bytes[offset..offset + 4] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)? + ); + offset += 4; + + if kernel_version != KERNEL_VERSION { + return Err(CodecError::InvalidVersion { + expected: KERNEL_VERSION, + actual: kernel_version, + }); } - + + // agent_id let agent_id: [u8; 32] = bytes[offset..offset + 32] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)?; offset += 32; - - if bytes.len() < offset + 4 { - return Err(CodecError::UnexpectedEndOfInput); - } - + + // agent_code_hash + let agent_code_hash: [u8; 32] = bytes[offset..offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + offset += 32; + + // constraint_set_hash + let constraint_set_hash: [u8; 32] = bytes[offset..offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + offset += 32; + + // input_root + let input_root: [u8; 32] = bytes[offset..offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + offset += 32; + + // execution_nonce + let execution_nonce = u64::from_le_bytes( + bytes[offset..offset + 8] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)? + ); + offset += 8; + + // opaque_agent_inputs length let agent_input_len_u32 = u32::from_le_bytes( bytes[offset..offset + 4] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)? ); - + if agent_input_len_u32 > MAX_AGENT_INPUT_BYTES as u32 { return Err(CodecError::InputTooLarge { size: agent_input_len_u32, limit: MAX_AGENT_INPUT_BYTES, }); } - + let agent_input_len = agent_input_len_u32 as usize; offset += 4; - + if bytes.len() < offset + agent_input_len { return Err(CodecError::UnexpectedEndOfInput); } - - let agent_input = bytes[offset..offset + agent_input_len].to_vec(); + + let opaque_agent_inputs = bytes[offset..offset + agent_input_len].to_vec(); offset += agent_input_len; - + if offset != bytes.len() { return Err(CodecError::InvalidLength); } - + Ok(KernelInputV1 { protocol_version, + kernel_version, agent_id, - agent_input, + agent_code_hash, + constraint_set_hash, + input_root, + execution_nonce, + opaque_agent_inputs, }) } } +/// KernelJournalV1 encoding layout (little-endian): +/// - protocol_version: u32 (4 bytes) +/// - kernel_version: u32 (4 bytes) +/// - agent_id: [u8; 32] (32 bytes) +/// - agent_code_hash: [u8; 32] (32 bytes) +/// - constraint_set_hash: [u8; 32] (32 bytes) +/// - input_root: [u8; 32] (32 bytes) +/// - execution_nonce: u64 (8 bytes) +/// - input_commitment: [u8; 32] (32 bytes) +/// - action_commitment: [u8; 32] (32 bytes) +/// - execution_status: u8 (1 byte) +/// +/// Total fixed size: 4+4+32+32+32+32+8+32+32+1 = 209 bytes +const JOURNAL_SIZE: usize = 209; + impl CanonicalEncode for KernelJournalV1 { fn encode(&self) -> Vec { - let mut buf = Vec::new(); - + let mut buf = Vec::with_capacity(JOURNAL_SIZE); + buf.extend_from_slice(&self.protocol_version.to_le_bytes()); buf.extend_from_slice(&self.kernel_version.to_le_bytes()); + buf.extend_from_slice(&self.agent_id); + buf.extend_from_slice(&self.agent_code_hash); + buf.extend_from_slice(&self.constraint_set_hash); + buf.extend_from_slice(&self.input_root); + buf.extend_from_slice(&self.execution_nonce.to_le_bytes()); buf.extend_from_slice(&self.input_commitment); buf.extend_from_slice(&self.action_commitment); + + // ExecutionStatus encoding: Success = 0x00 buf.push(match self.execution_status { - ExecutionStatus::Success => 0, + ExecutionStatus::Success => 0x00, }); - + + debug_assert_eq!(buf.len(), JOURNAL_SIZE); buf } } impl CanonicalDecode for KernelJournalV1 { fn decode(bytes: &[u8]) -> Result { - if bytes.len() != 73 { + if bytes.len() != JOURNAL_SIZE { return Err(CodecError::InvalidLength); } - + let mut offset = 0; - + let protocol_version = u32::from_le_bytes( bytes[offset..offset + 4] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)? ); offset += 4; - + let kernel_version = u32::from_le_bytes( bytes[offset..offset + 4] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)? ); offset += 4; - + + let agent_id: [u8; 32] = bytes[offset..offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + offset += 32; + + let agent_code_hash: [u8; 32] = bytes[offset..offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + offset += 32; + + let constraint_set_hash: [u8; 32] = bytes[offset..offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + offset += 32; + + let input_root: [u8; 32] = bytes[offset..offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + offset += 32; + + let execution_nonce = u64::from_le_bytes( + bytes[offset..offset + 8] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)? + ); + offset += 8; + let input_commitment: [u8; 32] = bytes[offset..offset + 32] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)?; offset += 32; - + let action_commitment: [u8; 32] = bytes[offset..offset + 32] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)?; offset += 32; - + + // ExecutionStatus decoding: 0x00 = Success, anything else is invalid let execution_status = match bytes[offset] { - 0 => ExecutionStatus::Success, + 0x00 => ExecutionStatus::Success, status => return Err(CodecError::InvalidExecutionStatus(status)), }; - + Ok(KernelJournalV1 { protocol_version, kernel_version, + agent_id, + agent_code_hash, + constraint_set_hash, + input_root, + execution_nonce, input_commitment, action_commitment, execution_status, @@ -161,21 +278,117 @@ impl CanonicalDecode for KernelJournalV1 { } } -impl CanonicalEncode for AgentOutput { +/// ActionV1 encoding layout (little-endian): +/// - action_type: u32 (4 bytes) +/// - target: [u8; 32] (32 bytes) +/// - payload_len: u32 (4 bytes) +/// - payload: [u8; len] (variable) +/// +/// Fixed header: 40 bytes + variable payload +impl CanonicalEncode for ActionV1 { fn encode(&self) -> Vec { - let data_len = self.data.len(); - if data_len > u32::MAX as usize { - panic!("Output data too large for u32 length prefix"); + let payload_len = self.payload.len(); + if payload_len > MAX_ACTION_PAYLOAD_BYTES { + panic!("Action payload exceeds maximum size"); } - if data_len > MAX_AGENT_OUTPUT_BYTES { - panic!("Output data exceeds maximum allowed size"); + if payload_len > u32::MAX as usize { + panic!("Payload too large for u32 length prefix"); } - - let total_len = 4 + data_len; + + let total_len = 4 + 32 + 4 + payload_len; let mut buf = Vec::with_capacity(total_len); - - buf.extend_from_slice(&(data_len as u32).to_le_bytes()); - buf.extend_from_slice(&self.data); + + buf.extend_from_slice(&self.action_type.to_le_bytes()); + buf.extend_from_slice(&self.target); + buf.extend_from_slice(&(payload_len as u32).to_le_bytes()); + buf.extend_from_slice(&self.payload); + + buf + } +} + +impl CanonicalDecode for ActionV1 { + fn decode(bytes: &[u8]) -> Result { + // Minimum: action_type (4) + target (32) + payload_len (4) = 40 bytes + if bytes.len() < 40 { + return Err(CodecError::UnexpectedEndOfInput); + } + + let mut offset = 0; + + let action_type = u32::from_le_bytes( + bytes[offset..offset + 4] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)? + ); + offset += 4; + + let target: [u8; 32] = bytes[offset..offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + offset += 32; + + let payload_len_u32 = u32::from_le_bytes( + bytes[offset..offset + 4] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)? + ); + + if payload_len_u32 > MAX_ACTION_PAYLOAD_BYTES as u32 { + return Err(CodecError::ActionPayloadTooLarge { + size: payload_len_u32, + limit: MAX_ACTION_PAYLOAD_BYTES, + }); + } + + let payload_len = payload_len_u32 as usize; + offset += 4; + + if bytes.len() < offset + payload_len { + return Err(CodecError::UnexpectedEndOfInput); + } + + let payload = bytes[offset..offset + payload_len].to_vec(); + offset += payload_len; + + if offset != bytes.len() { + return Err(CodecError::InvalidLength); + } + + Ok(ActionV1 { + action_type, + target, + payload, + }) + } +} + +/// AgentOutput encoding layout (little-endian): +/// - action_count: u32 (4 bytes) +/// - actions: [ActionV1; count] (variable, each action is variable-length) +/// +/// Actions are encoded sequentially without additional framing. +impl CanonicalEncode for AgentOutput { + fn encode(&self) -> Vec { + let action_count = self.actions.len(); + if action_count > MAX_ACTIONS_PER_OUTPUT { + panic!("Too many actions in output"); + } + if action_count > u32::MAX as usize { + panic!("Action count too large for u32"); + } + + // Estimate capacity: 4 bytes for count + ~100 bytes per action average + let mut buf = Vec::with_capacity(4 + action_count * 100); + + buf.extend_from_slice(&(action_count as u32).to_le_bytes()); + + for action in &self.actions { + let action_bytes = action.encode(); + buf.extend_from_slice(&(action_bytes.len() as u32).to_le_bytes()); + buf.extend_from_slice(&action_bytes); + } + buf } } @@ -185,28 +398,54 @@ impl CanonicalDecode for AgentOutput { if bytes.len() < 4 { return Err(CodecError::UnexpectedEndOfInput); } - - let data_len_u32 = u32::from_le_bytes( - bytes[0..4] + + let mut offset = 0; + + let action_count_u32 = u32::from_le_bytes( + bytes[offset..offset + 4] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)? ); - - if data_len_u32 > MAX_AGENT_OUTPUT_BYTES as u32 { - return Err(CodecError::OutputTooLarge { - size: data_len_u32, - limit: MAX_AGENT_OUTPUT_BYTES, + + if action_count_u32 > MAX_ACTIONS_PER_OUTPUT as u32 { + return Err(CodecError::TooManyActions { + count: action_count_u32, + limit: MAX_ACTIONS_PER_OUTPUT, }); } - - let data_len = data_len_u32 as usize; - - if bytes.len() != 4 + data_len { + + let action_count = action_count_u32 as usize; + offset += 4; + + let mut actions = Vec::with_capacity(action_count); + + for _ in 0..action_count { + // Read action length prefix + if bytes.len() < offset + 4 { + return Err(CodecError::UnexpectedEndOfInput); + } + + let action_len_u32 = u32::from_le_bytes( + bytes[offset..offset + 4] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)? + ); + let action_len = action_len_u32 as usize; + offset += 4; + + if bytes.len() < offset + action_len { + return Err(CodecError::UnexpectedEndOfInput); + } + + let action = ActionV1::decode(&bytes[offset..offset + action_len])?; + actions.push(action); + offset += action_len; + } + + if offset != bytes.len() { return Err(CodecError::InvalidLength); } - - let data = bytes[4..4 + data_len].to_vec(); - - Ok(AgentOutput { data }) + + Ok(AgentOutput { actions }) } -} \ No newline at end of file +} diff --git a/crates/kernel-core/src/lib.rs b/crates/kernel-core/src/lib.rs index f30c705..4e9d66e 100644 --- a/crates/kernel-core/src/lib.rs +++ b/crates/kernel-core/src/lib.rs @@ -6,8 +6,17 @@ pub use types::*; pub use codec::*; pub use hash::*; +/// Protocol version for wire format compatibility pub const PROTOCOL_VERSION: u32 = 1; + +/// Kernel version declaring execution semantics pub const KERNEL_VERSION: u32 = 1; + +/// Maximum size of opaque agent inputs (64KB) pub const MAX_AGENT_INPUT_BYTES: usize = 64_000; + +/// Maximum total size of agent output when encoded pub const MAX_AGENT_OUTPUT_BYTES: usize = 64_000; -pub const MAX_ALLOCATION_BYTES: usize = 1_000_000; \ No newline at end of file + +/// Maximum memory allocation for bounded execution +pub const MAX_ALLOCATION_BYTES: usize = 1_000_000; diff --git a/crates/kernel-core/src/types.rs b/crates/kernel-core/src/types.rs index 79bb6f6..ffb3534 100644 --- a/crates/kernel-core/src/types.rs +++ b/crates/kernel-core/src/types.rs @@ -1,27 +1,111 @@ +/// Kernel input structure for P0.1 protocol. +/// +/// Contains all consensus-critical fields needed to bind the proof to: +/// - The specific kernel semantics (kernel_version) +/// - The agent code being executed (agent_code_hash) +/// - The constraint policy enforced (constraint_set_hash) +/// - The external state observed (input_root) +/// - Replay protection (execution_nonce) #[derive(Clone, Debug, PartialEq)] pub struct KernelInputV1 { + /// Protocol version for wire format compatibility pub protocol_version: u32, + /// Kernel version declaring which semantics are being proven + pub kernel_version: u32, + /// 32-byte agent identifier pub agent_id: [u8; 32], - pub agent_input: Vec, + /// SHA-256 hash of the agent binary/code + pub agent_code_hash: [u8; 32], + /// SHA-256 hash of the constraint set being enforced + pub constraint_set_hash: [u8; 32], + /// External state root (market/vault snapshot) the agent observes + pub input_root: [u8; 32], + /// Monotonic nonce for replay protection + pub execution_nonce: u64, + /// Opaque agent-specific input data (max 64KB) + pub opaque_agent_inputs: Vec, } +/// Kernel journal (output) structure for P0.1 protocol. +/// +/// Contains all fields needed for on-chain verification: +/// - Identity fields (agent_id, agent_code_hash) for binding to strategy +/// - Constraint policy (constraint_set_hash) for proving policy enforcement +/// - Replay protection (execution_nonce) for ordering/dedup +/// - Cryptographic commitments for input/output verification +/// +/// Journal size: 169 bytes fixed (4+4+32+32+32+32+8+32+32+1) #[derive(Clone, Debug, PartialEq)] pub struct KernelJournalV1 { + /// Protocol version for wire format compatibility pub protocol_version: u32, + /// Kernel version that produced this journal pub kernel_version: u32, + /// Agent identifier (copied from input for verifier convenience) + pub agent_id: [u8; 32], + /// Agent code hash (proof binds to this specific agent) + pub agent_code_hash: [u8; 32], + /// Constraint set hash (proof binds to this policy) + pub constraint_set_hash: [u8; 32], + /// Input root (external state that was observed) + pub input_root: [u8; 32], + /// Execution nonce for replay protection + pub execution_nonce: u64, + /// SHA-256(full_input_bytes) - commits to entire input pub input_commitment: [u8; 32], + /// SHA-256(agent_output_bytes) - commits to actions pub action_commitment: [u8; 32], + /// Execution result status pub execution_status: ExecutionStatus, } +/// Execution status enum. +/// +/// Encoding: Success = 0x00 +/// Any other value is invalid and must be rejected on decode. +/// +/// For P0.1, only Success is defined. Failure cases abort before +/// journal commit, so no failure status is needed in the journal. #[derive(Clone, Debug, PartialEq)] pub enum ExecutionStatus { + /// Execution completed successfully. Encoded as 0x00. Success, } +/// Structured action format for agent output. +/// +/// Each action has: +/// - action_type: 4-byte identifier for the action kind +/// - target: 32-byte target address/identifier +/// - payload: Variable-length action data (max 16KB per action) +/// +/// Actions are ordered and the ordering is consensus-critical. +/// The kernel enforces deterministic ordering by requiring agents +/// to produce actions in their canonical order. +#[derive(Clone, Debug, PartialEq)] +pub struct ActionV1 { + /// 4-byte action type identifier + pub action_type: u32, + /// 32-byte target address/identifier + pub target: [u8; 32], + /// Action-specific payload (max 16KB) + pub payload: Vec, +} + +/// Maximum payload size per action (16KB) +pub const MAX_ACTION_PAYLOAD_BYTES: usize = 16_384; + +/// Maximum number of actions per output +pub const MAX_ACTIONS_PER_OUTPUT: usize = 64; + +/// Structured agent output containing ordered actions. +/// +/// Actions must be in canonical order as produced by the agent. +/// The action_commitment is computed over the encoded AgentOutput. #[derive(Clone, Debug, PartialEq)] pub struct AgentOutput { - pub data: Vec, + /// Ordered list of actions (max 64 actions) + pub actions: Vec, } #[derive(Clone, Debug, PartialEq)] @@ -33,4 +117,57 @@ pub enum CodecError { UnexpectedEndOfInput, InvalidExecutionStatus(u8), ArithmeticOverflow, -} \ No newline at end of file + TooManyActions { count: u32, limit: usize }, + ActionPayloadTooLarge { size: u32, limit: usize }, +} + +/// Kernel-level execution errors. +/// +/// Separate from CodecError to distinguish parsing failures from +/// execution failures. All errors result in kernel abort before +/// journal commit. +#[derive(Clone, Debug, PartialEq)] +pub enum KernelError { + /// Input decoding failed + Codec(CodecError), + /// Protocol version not supported + UnsupportedProtocolVersion { expected: u32, actual: u32 }, + /// Kernel version not supported + UnsupportedKernelVersion { expected: u32, actual: u32 }, + /// Agent execution failed + AgentExecutionFailed(AgentError), + /// Constraint check failed + ConstraintViolation(ConstraintError), + /// Agent ID validation failed + InvalidAgentId, + /// Agent code hash mismatch + AgentCodeHashMismatch, +} + +/// Agent execution errors +#[derive(Clone, Debug, PartialEq)] +pub enum AgentError { + /// Input data is invalid for this agent + InvalidInput, + /// Agent panicked or failed during execution + ExecutionFailed, + /// Output exceeds size limits + OutputTooLarge, + /// Too many actions produced + TooManyActions, +} + +/// Constraint checking errors +#[derive(Clone, Debug, PartialEq)] +pub enum ConstraintError { + /// An action violated a constraint + ViolatedConstraint { action_index: usize, reason: &'static str }, + /// Output structure is invalid + InvalidOutput, +} + +impl From for KernelError { + fn from(e: CodecError) -> Self { + KernelError::Codec(e) + } +} diff --git a/crates/kernel-guest/src/lib.rs b/crates/kernel-guest/src/lib.rs index 2bc4168..ffcaf23 100644 --- a/crates/kernel-guest/src/lib.rs +++ b/crates/kernel-guest/src/lib.rs @@ -1,58 +1,101 @@ use kernel_core::*; -use agent_traits::{Agent, TrivialAgent}; -use constraints::{check, Meta}; - -#[derive(Clone, Debug, PartialEq)] -pub enum KernelError { - InvalidInput(CodecError), - UnsupportedProtocolVersion(u32), - UnsupportedKernelVersion(u32), - InputTooLarge(usize), - AgentExecutionFailed, - ConstraintViolation, - InvalidAgentOutput, -} - -impl From for KernelError { - fn from(error: CodecError) -> Self { - match error { - CodecError::InputTooLarge { size, .. } => KernelError::InputTooLarge(size as usize), - CodecError::OutputTooLarge { .. } => KernelError::InvalidAgentOutput, - other => KernelError::InvalidInput(other), - } - } -} +use agent_traits::{Agent, AgentContext, TrivialAgent}; +use constraints::{check, validate_output_structure, ConstraintMeta}; +/// Main kernel execution function. +/// +/// This is the core execution logic that: +/// 1. Decodes and validates input +/// 2. Verifies protocol and kernel versions +/// 3. Computes input commitment +/// 4. Executes the agent +/// 5. Validates and checks constraints on output +/// 6. Computes action commitment +/// 7. Constructs and returns the journal +/// +/// # Arguments +/// * `input_bytes` - Canonical encoding of KernelInputV1 +/// +/// # Returns +/// * `Ok(Vec)` - Canonical encoding of KernelJournalV1 +/// * `Err(KernelError)` - Execution failed, no journal produced +/// +/// # Determinism +/// +/// This function is fully deterministic. Same input bytes will +/// always produce the same output bytes across: +/// - Different machines +/// - Different provers +/// - Rebuilds with pinned toolchain pub fn kernel_main(input_bytes: &[u8]) -> Result, KernelError> { + // 1. Decode input let input = KernelInputV1::decode(input_bytes)?; - + + // 2. Validate versions (already checked in decode, but be explicit) if input.protocol_version != PROTOCOL_VERSION { - return Err(KernelError::UnsupportedProtocolVersion(input.protocol_version)); + return Err(KernelError::UnsupportedProtocolVersion { + expected: PROTOCOL_VERSION, + actual: input.protocol_version, + }); } - + + if input.kernel_version != KERNEL_VERSION { + return Err(KernelError::UnsupportedKernelVersion { + expected: KERNEL_VERSION, + actual: input.kernel_version, + }); + } + + // 3. Compute input commitment (over full input bytes) let input_commitment = compute_input_commitment(input_bytes); - - let agent_output = TrivialAgent::run(input.agent_id, &input.agent_input) - .map_err(|_| KernelError::AgentExecutionFailed)?; - - - let meta = Meta { + + // 4. Build agent context from input + let agent_ctx = AgentContext { + agent_id: input.agent_id, + agent_code_hash: input.agent_code_hash, + constraint_set_hash: input.constraint_set_hash, + input_root: input.input_root, + execution_nonce: input.execution_nonce, + }; + + // 5. Execute agent + let agent_output = TrivialAgent::run(&agent_ctx, &input.opaque_agent_inputs) + .map_err(KernelError::AgentExecutionFailed)?; + + // 6. Validate output structure + validate_output_structure(&agent_output) + .map_err(KernelError::ConstraintViolation)?; + + // 7. Build constraint metadata + let constraint_meta = ConstraintMeta { agent_id: input.agent_id, + agent_code_hash: input.agent_code_hash, + constraint_set_hash: input.constraint_set_hash, + input_root: input.input_root, + execution_nonce: input.execution_nonce, }; - - check(&agent_output, &meta) - .map_err(|_| KernelError::ConstraintViolation)?; - + + // 8. Check constraints (MANDATORY) + check(&agent_output, &constraint_meta) + .map_err(KernelError::ConstraintViolation)?; + + // 9. Compute action commitment let agent_output_bytes = agent_output.encode(); let action_commitment = compute_action_commitment(&agent_output_bytes); - + + // 10. Construct journal with all identity and commitment fields let journal = KernelJournalV1 { protocol_version: PROTOCOL_VERSION, kernel_version: KERNEL_VERSION, + agent_id: input.agent_id, + agent_code_hash: input.agent_code_hash, + constraint_set_hash: input.constraint_set_hash, + input_root: input.input_root, + execution_nonce: input.execution_nonce, input_commitment, action_commitment, execution_status: ExecutionStatus::Success, }; - + Ok(journal.encode()) -} \ No newline at end of file +} From 45d4278538014bfb80618b0363723f6e1851041f Mon Sep 17 00:00:00 2001 From: mehdi-defiesta Date: Wed, 21 Jan 2026 19:48:54 +0100 Subject: [PATCH 2/3] Make CanonicalEncode fallible for P0.1 protocol tightening - Change CanonicalEncode::encode() to return Result, CodecError> - Add KernelError::EncodingFailed variant for encoding error propagation - Enforce MAX_AGENT_INPUT_BYTES on encode side (not just decode) - Use ArithmeticOverflow for u32 overflow edge cases - Fix KernelJournalV1::decode offset tracking with debug_assert - Update kernel_main to propagate encoding errors - Update all tests to handle Result type --- crates/host-tests/src/lib.rs | 148 ++++++++++++++++++++++++++------ crates/kernel-core/src/codec.rs | 91 +++++++++++++++----- crates/kernel-core/src/types.rs | 76 ++++++++++++++-- crates/kernel-guest/src/lib.rs | 7 +- 4 files changed, 263 insertions(+), 59 deletions(-) diff --git a/crates/host-tests/src/lib.rs b/crates/host-tests/src/lib.rs index 085732e..7a5bfe4 100644 --- a/crates/host-tests/src/lib.rs +++ b/crates/host-tests/src/lib.rs @@ -21,7 +21,7 @@ mod tests { fn test_kernel_input_encoding_round_trip() { let original = make_input(vec![1, 2, 3, 4, 5]); - let encoded = original.encode(); + let encoded = original.encode().unwrap(); let decoded = KernelInputV1::decode(&encoded).unwrap(); assert_eq!(original, decoded); @@ -42,7 +42,7 @@ mod tests { execution_status: ExecutionStatus::Success, }; - let encoded = original.encode(); + let encoded = original.encode().unwrap(); let decoded = KernelJournalV1::decode(&encoded).unwrap(); assert_eq!(original, decoded); @@ -56,7 +56,7 @@ mod tests { payload: vec![10, 20, 30, 40], }; - let encoded = original.encode(); + let encoded = original.encode().unwrap(); let decoded = ActionV1::decode(&encoded).unwrap(); assert_eq!(original, decoded); @@ -79,7 +79,7 @@ mod tests { ], }; - let encoded = original.encode(); + let encoded = original.encode().unwrap(); let decoded = AgentOutput::decode(&encoded).unwrap(); assert_eq!(original, decoded); @@ -89,13 +89,59 @@ mod tests { fn test_empty_agent_output_encoding() { let original = AgentOutput { actions: vec![] }; - let encoded = original.encode(); + let encoded = original.encode().unwrap(); let decoded = AgentOutput::decode(&encoded).unwrap(); assert_eq!(original, decoded); assert_eq!(encoded.len(), 4); // Just the count field } + #[test] + fn test_action_canonicalization() { + // Create actions in non-canonical order + let actions_unordered = vec![ + ActionV1 { + action_type: 2, // Higher type + target: [0x11; 32], + payload: vec![1], + }, + ActionV1 { + action_type: 1, // Lower type - should sort first + target: [0x22; 32], + payload: vec![2], + }, + ActionV1 { + action_type: 1, // Same type, different target + target: [0x11; 32], // Lower target - should sort before [0x22] + payload: vec![3], + }, + ]; + + let output1 = AgentOutput { actions: actions_unordered.clone() }; + let canonical1 = output1.into_canonical(); + + // Verify ordering: action_type ascending, then target lexicographic + assert_eq!(canonical1.actions[0].action_type, 1); + assert_eq!(canonical1.actions[0].target, [0x11; 32]); + assert_eq!(canonical1.actions[0].payload, vec![3]); + + assert_eq!(canonical1.actions[1].action_type, 1); + assert_eq!(canonical1.actions[1].target, [0x22; 32]); + assert_eq!(canonical1.actions[1].payload, vec![2]); + + assert_eq!(canonical1.actions[2].action_type, 2); + assert_eq!(canonical1.actions[2].target, [0x11; 32]); + assert_eq!(canonical1.actions[2].payload, vec![1]); + + // Different initial order should produce same canonical output + let actions_reversed: Vec = actions_unordered.iter().rev().cloned().collect(); + let output2 = AgentOutput { actions: actions_reversed }; + let canonical2 = output2.into_canonical(); + + // Encoding should be identical regardless of initial order + assert_eq!(canonical1.encode().unwrap(), canonical2.encode().unwrap()); + } + #[test] fn test_input_commitment_golden_vector() { // Using simple input bytes for reproducible test @@ -117,7 +163,7 @@ mod tests { fn test_action_commitment_golden_vector() { // Empty actions list encodes to [0, 0, 0, 0] (count = 0) let agent_output = AgentOutput { actions: vec![] }; - let output_bytes = agent_output.encode(); + let output_bytes = agent_output.encode().unwrap(); let commitment = compute_action_commitment(&output_bytes); // SHA256([0, 0, 0, 0]) - empty action list @@ -134,7 +180,7 @@ mod tests { #[test] fn test_determinism() { let input = make_input(vec![100, 200]); - let input_bytes = input.encode(); + let input_bytes = input.encode().unwrap(); let result1 = kernel_main(&input_bytes).unwrap(); let result2 = kernel_main(&input_bytes).unwrap(); @@ -147,7 +193,7 @@ mod tests { let mut input = make_input(vec![1, 2, 3]); input.protocol_version = 999; - let input_bytes = input.encode(); + let input_bytes = input.encode().unwrap(); let result = KernelInputV1::decode(&input_bytes); assert!(matches!(result, Err(CodecError::InvalidVersion { expected: 1, actual: 999 }))); @@ -158,20 +204,65 @@ mod tests { let mut input = make_input(vec![1, 2, 3]); input.kernel_version = 999; - let input_bytes = input.encode(); + let input_bytes = input.encode().unwrap(); let result = KernelInputV1::decode(&input_bytes); assert!(matches!(result, Err(CodecError::InvalidVersion { expected: 1, actual: 999 }))); } + #[test] + fn test_journal_invalid_protocol_version() { + let journal = KernelJournalV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0; 32], + agent_code_hash: [0; 32], + constraint_set_hash: [0; 32], + input_root: [0; 32], + execution_nonce: 0, + input_commitment: [0; 32], + action_commitment: [0; 32], + execution_status: ExecutionStatus::Success, + }; + + let mut encoded = journal.encode().unwrap(); + // Corrupt protocol version to 999 (little-endian) + encoded[0..4].copy_from_slice(&999u32.to_le_bytes()); + + let result = KernelJournalV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidVersion { expected: 1, actual: 999 }))); + } + + #[test] + fn test_journal_invalid_kernel_version() { + let journal = KernelJournalV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0; 32], + agent_code_hash: [0; 32], + constraint_set_hash: [0; 32], + input_root: [0; 32], + execution_nonce: 0, + input_commitment: [0; 32], + action_commitment: [0; 32], + execution_status: ExecutionStatus::Success, + }; + + let mut encoded = journal.encode().unwrap(); + // Corrupt kernel version to 999 (at offset 4, little-endian) + encoded[4..8].copy_from_slice(&999u32.to_le_bytes()); + + let result = KernelJournalV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidVersion { expected: 1, actual: 999 }))); + } + #[test] fn test_input_too_large() { let large_input = vec![0u8; MAX_AGENT_INPUT_BYTES + 1]; let input = make_input(large_input); - let input_bytes = input.encode(); - let result = KernelInputV1::decode(&input_bytes); - + // Encode-side now catches oversized inputs + let result = input.encode(); assert!(matches!(result, Err(CodecError::InputTooLarge { .. }))); } @@ -198,7 +289,7 @@ mod tests { execution_status: ExecutionStatus::Success, }; - let encoded = journal.encode(); + let encoded = journal.encode().unwrap(); // protocol_version: 4 + kernel_version: 4 + agent_id: 32 + // agent_code_hash: 32 + constraint_set_hash: 32 + input_root: 32 + // execution_nonce: 8 + input_commitment: 32 + action_commitment: 32 + @@ -209,7 +300,7 @@ mod tests { #[test] fn test_constraints_enforcement() { let input = make_input(vec![1, 2, 3]); - let input_bytes = input.encode(); + let input_bytes = input.encode().unwrap(); let result = kernel_main(&input_bytes); assert!(result.is_ok()); @@ -218,7 +309,7 @@ mod tests { #[test] fn test_empty_input() { let input = make_input(vec![]); - let input_bytes = input.encode(); + let input_bytes = input.encode().unwrap(); let result = kernel_main(&input_bytes); assert!(result.is_ok()); @@ -231,7 +322,7 @@ mod tests { #[test] fn test_max_size_input() { let input = make_input(vec![0x42; MAX_AGENT_INPUT_BYTES]); - let input_bytes = input.encode(); + let input_bytes = input.encode().unwrap(); let result = kernel_main(&input_bytes); assert!(result.is_ok()); @@ -250,7 +341,7 @@ mod tests { opaque_agent_inputs: vec![1, 2, 3], }; - let input_bytes = input.encode(); + let input_bytes = input.encode().unwrap(); let journal_bytes = kernel_main(&input_bytes).unwrap(); let journal = KernelJournalV1::decode(&journal_bytes).unwrap(); @@ -289,7 +380,7 @@ mod tests { #[test] fn test_execution_status_encoding() { - // Success encodes as 0x00 + // Success encodes as 0x01 (0x00 reserved to catch uninitialized memory) let journal = KernelJournalV1 { protocol_version: PROTOCOL_VERSION, kernel_version: KERNEL_VERSION, @@ -303,9 +394,9 @@ mod tests { execution_status: ExecutionStatus::Success, }; - let encoded = journal.encode(); - // Last byte should be 0x00 for Success - assert_eq!(*encoded.last().unwrap(), 0x00); + let encoded = journal.encode().unwrap(); + // Last byte should be 0x01 for Success + assert_eq!(*encoded.last().unwrap(), 0x01); } #[test] @@ -323,12 +414,17 @@ mod tests { execution_status: ExecutionStatus::Success, }; - let mut encoded = journal.encode(); + let mut encoded = journal.encode().unwrap(); // Corrupt the status byte to an invalid value *encoded.last_mut().unwrap() = 0xFF; let result = KernelJournalV1::decode(&encoded); assert!(matches!(result, Err(CodecError::InvalidExecutionStatus(0xFF)))); + + // Also verify that 0x00 is invalid (reserved to catch uninitialized memory) + *encoded.last_mut().unwrap() = 0x00; + let result = KernelJournalV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidExecutionStatus(0x00)))); } #[test] @@ -342,7 +438,7 @@ mod tests { for test_input in test_cases { let input = make_input(test_input); - let input_bytes = input.encode(); + let input_bytes = input.encode().unwrap(); // Run multiple times to ensure determinism let result1 = kernel_main(&input_bytes).unwrap(); @@ -372,8 +468,8 @@ mod tests { ..input1.clone() }; - let journal1 = KernelJournalV1::decode(&kernel_main(&input1.encode()).unwrap()).unwrap(); - let journal2 = KernelJournalV1::decode(&kernel_main(&input2.encode()).unwrap()).unwrap(); + let journal1 = KernelJournalV1::decode(&kernel_main(&input1.encode().unwrap()).unwrap()).unwrap(); + let journal2 = KernelJournalV1::decode(&kernel_main(&input2.encode().unwrap()).unwrap()).unwrap(); assert_eq!(journal1.execution_nonce, 1); assert_eq!(journal2.execution_nonce, 2); @@ -386,7 +482,7 @@ mod tests { fn test_input_header_size() { // Verify minimum input size with empty data let input = make_input(vec![]); - let encoded = input.encode(); + let encoded = input.encode().unwrap(); // Fixed fields (144) + length prefix (4) + 0 bytes data = 148 assert_eq!(encoded.len(), 148); diff --git a/crates/kernel-core/src/codec.rs b/crates/kernel-core/src/codec.rs index 0cd8549..0fae9f9 100644 --- a/crates/kernel-core/src/codec.rs +++ b/crates/kernel-core/src/codec.rs @@ -2,7 +2,7 @@ use crate::types::*; use crate::{MAX_AGENT_INPUT_BYTES, PROTOCOL_VERSION, KERNEL_VERSION}; pub trait CanonicalEncode { - fn encode(&self) -> Vec; + fn encode(&self) -> Result, CodecError>; } pub trait CanonicalDecode: Sized { @@ -23,10 +23,16 @@ pub trait CanonicalDecode: Sized { /// Fixed header: 144 bytes + 4 byte length prefix + variable input data /// Minimum size with empty input: 148 bytes impl CanonicalEncode for KernelInputV1 { - fn encode(&self) -> Vec { + fn encode(&self) -> Result, CodecError> { let data_len = self.opaque_agent_inputs.len(); + if data_len > MAX_AGENT_INPUT_BYTES { + return Err(CodecError::InputTooLarge { + size: data_len.min(u32::MAX as usize) as u32, + limit: MAX_AGENT_INPUT_BYTES, + }); + } if data_len > u32::MAX as usize { - panic!("Input data too large for u32 length prefix"); + return Err(CodecError::ArithmeticOverflow); } // Fixed fields (144) + length prefix (4) + data @@ -43,7 +49,7 @@ impl CanonicalEncode for KernelInputV1 { buf.extend_from_slice(&(data_len as u32).to_le_bytes()); buf.extend_from_slice(&self.opaque_agent_inputs); - buf + Ok(buf) } } @@ -175,7 +181,7 @@ impl CanonicalDecode for KernelInputV1 { const JOURNAL_SIZE: usize = 209; impl CanonicalEncode for KernelJournalV1 { - fn encode(&self) -> Vec { + fn encode(&self) -> Result, CodecError> { let mut buf = Vec::with_capacity(JOURNAL_SIZE); buf.extend_from_slice(&self.protocol_version.to_le_bytes()); @@ -188,13 +194,13 @@ impl CanonicalEncode for KernelJournalV1 { buf.extend_from_slice(&self.input_commitment); buf.extend_from_slice(&self.action_commitment); - // ExecutionStatus encoding: Success = 0x00 + // ExecutionStatus encoding: Success = 0x01 (0x00 reserved to catch uninitialized memory) buf.push(match self.execution_status { - ExecutionStatus::Success => 0x00, + ExecutionStatus::Success => 0x01, }); debug_assert_eq!(buf.len(), JOURNAL_SIZE); - buf + Ok(buf) } } @@ -213,6 +219,14 @@ impl CanonicalDecode for KernelJournalV1 { ); offset += 4; + // Validate protocol version for upgrade safety + if protocol_version != PROTOCOL_VERSION { + return Err(CodecError::InvalidVersion { + expected: PROTOCOL_VERSION, + actual: protocol_version, + }); + } + let kernel_version = u32::from_le_bytes( bytes[offset..offset + 4] .try_into() @@ -220,6 +234,14 @@ impl CanonicalDecode for KernelJournalV1 { ); offset += 4; + // Validate kernel version for upgrade safety + if kernel_version != KERNEL_VERSION { + return Err(CodecError::InvalidVersion { + expected: KERNEL_VERSION, + actual: kernel_version, + }); + } + let agent_id: [u8; 32] = bytes[offset..offset + 32] .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)?; @@ -257,11 +279,13 @@ impl CanonicalDecode for KernelJournalV1 { .map_err(|_| CodecError::UnexpectedEndOfInput)?; offset += 32; - // ExecutionStatus decoding: 0x00 = Success, anything else is invalid + // ExecutionStatus decoding: 0x01 = Success, 0x00 and anything else is invalid let execution_status = match bytes[offset] { - 0x00 => ExecutionStatus::Success, + 0x01 => ExecutionStatus::Success, status => return Err(CodecError::InvalidExecutionStatus(status)), }; + offset += 1; + debug_assert_eq!(offset, JOURNAL_SIZE); Ok(KernelJournalV1 { protocol_version, @@ -286,13 +310,16 @@ impl CanonicalDecode for KernelJournalV1 { /// /// Fixed header: 40 bytes + variable payload impl CanonicalEncode for ActionV1 { - fn encode(&self) -> Vec { + fn encode(&self) -> Result, CodecError> { let payload_len = self.payload.len(); if payload_len > MAX_ACTION_PAYLOAD_BYTES { - panic!("Action payload exceeds maximum size"); + return Err(CodecError::ActionPayloadTooLarge { + size: payload_len as u32, + limit: MAX_ACTION_PAYLOAD_BYTES, + }); } if payload_len > u32::MAX as usize { - panic!("Payload too large for u32 length prefix"); + return Err(CodecError::ArithmeticOverflow); } let total_len = 4 + 32 + 4 + payload_len; @@ -303,7 +330,7 @@ impl CanonicalEncode for ActionV1 { buf.extend_from_slice(&(payload_len as u32).to_le_bytes()); buf.extend_from_slice(&self.payload); - buf + Ok(buf) } } @@ -365,31 +392,42 @@ impl CanonicalDecode for ActionV1 { /// AgentOutput encoding layout (little-endian): /// - action_count: u32 (4 bytes) -/// - actions: [ActionV1; count] (variable, each action is variable-length) +/// - for each action: +/// - action_len: u32 (4 bytes) - length of the following action encoding +/// - action: ActionV1 encoding (variable) /// -/// Actions are encoded sequentially without additional framing. +/// IMPORTANT: Actions are automatically sorted into canonical order before encoding. +/// This ensures deterministic action_commitment regardless of the order agents produce actions. +/// Ordering: action_type (ascending) → target (lexicographic) → payload (lexicographic) impl CanonicalEncode for AgentOutput { - fn encode(&self) -> Vec { + fn encode(&self) -> Result, CodecError> { let action_count = self.actions.len(); if action_count > MAX_ACTIONS_PER_OUTPUT { - panic!("Too many actions in output"); + return Err(CodecError::TooManyActions { + count: action_count as u32, + limit: MAX_ACTIONS_PER_OUTPUT, + }); } if action_count > u32::MAX as usize { - panic!("Action count too large for u32"); + return Err(CodecError::ArithmeticOverflow); } + // Sort actions into canonical order for deterministic encoding + let mut sorted_actions = self.actions.clone(); + sorted_actions.sort(); + // Estimate capacity: 4 bytes for count + ~100 bytes per action average let mut buf = Vec::with_capacity(4 + action_count * 100); buf.extend_from_slice(&(action_count as u32).to_le_bytes()); - for action in &self.actions { - let action_bytes = action.encode(); + for action in &sorted_actions { + let action_bytes = action.encode()?; buf.extend_from_slice(&(action_bytes.len() as u32).to_le_bytes()); buf.extend_from_slice(&action_bytes); } - buf + Ok(buf) } } @@ -430,6 +468,15 @@ impl CanonicalDecode for AgentOutput { .try_into() .map_err(|_| CodecError::UnexpectedEndOfInput)? ); + + // Reject absurdly large action lengths before attempting allocation + if action_len_u32 > MAX_SINGLE_ACTION_BYTES as u32 { + return Err(CodecError::ActionTooLarge { + size: action_len_u32, + limit: MAX_SINGLE_ACTION_BYTES, + }); + } + let action_len = action_len_u32 as usize; offset += 4; diff --git a/crates/kernel-core/src/types.rs b/crates/kernel-core/src/types.rs index ffb3534..caa2f1a 100644 --- a/crates/kernel-core/src/types.rs +++ b/crates/kernel-core/src/types.rs @@ -34,7 +34,7 @@ pub struct KernelInputV1 { /// - Replay protection (execution_nonce) for ordering/dedup /// - Cryptographic commitments for input/output verification /// -/// Journal size: 169 bytes fixed (4+4+32+32+32+32+8+32+32+1) +/// Journal size: 209 bytes fixed (4+4+32+32+32+32+8+32+32+1) #[derive(Clone, Debug, PartialEq)] pub struct KernelJournalV1 { /// Protocol version for wire format compatibility @@ -61,14 +61,15 @@ pub struct KernelJournalV1 { /// Execution status enum. /// -/// Encoding: Success = 0x00 +/// Encoding: Success = 0x01 +/// 0x00 is reserved/invalid (prevents uninitialized memory from being interpreted as success). /// Any other value is invalid and must be rejected on decode. /// /// For P0.1, only Success is defined. Failure cases abort before /// journal commit, so no failure status is needed in the journal. #[derive(Clone, Debug, PartialEq)] pub enum ExecutionStatus { - /// Execution completed successfully. Encoded as 0x00. + /// Execution completed successfully. Encoded as 0x01. Success, } @@ -80,9 +81,15 @@ pub enum ExecutionStatus { /// - payload: Variable-length action data (max 16KB per action) /// /// Actions are ordered and the ordering is consensus-critical. -/// The kernel enforces deterministic ordering by requiring agents -/// to produce actions in their canonical order. -#[derive(Clone, Debug, PartialEq)] +/// The kernel enforces deterministic ordering by sorting actions +/// before commitment using lexicographic comparison: +/// 1. action_type (ascending) +/// 2. target (lexicographic) +/// 3. payload (lexicographic) +/// +/// This kernel-side canonicalization ensures determinism regardless +/// of the order in which agents produce actions. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ActionV1 { /// 4-byte action type identifier pub action_type: u32, @@ -98,16 +105,66 @@ pub const MAX_ACTION_PAYLOAD_BYTES: usize = 16_384; /// Maximum number of actions per output pub const MAX_ACTIONS_PER_OUTPUT: usize = 64; +/// Maximum encoded size of a single ActionV1. +/// Computed as: action_type (4) + target (32) + payload_len (4) + MAX_ACTION_PAYLOAD_BYTES +/// = 40 + 16384 = 16424 bytes +pub const MAX_SINGLE_ACTION_BYTES: usize = 40 + MAX_ACTION_PAYLOAD_BYTES; + /// Structured agent output containing ordered actions. /// -/// Actions must be in canonical order as produced by the agent. -/// The action_commitment is computed over the encoded AgentOutput. +/// Actions are sorted into canonical order by the kernel before +/// commitment computation (see ActionV1 for ordering rules). +/// The action_commitment is computed over the encoded AgentOutput +/// after canonicalization. #[derive(Clone, Debug, PartialEq)] pub struct AgentOutput { /// Ordered list of actions (max 64 actions) pub actions: Vec, } +// Manual Ord implementation for ActionV1 to ensure deterministic ordering. +// Ordering: action_type (ascending) → target (lexicographic) → payload (lexicographic) +impl Ord for ActionV1 { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + match self.action_type.cmp(&other.action_type) { + core::cmp::Ordering::Equal => {} + ord => return ord, + } + match self.target.cmp(&other.target) { + core::cmp::Ordering::Equal => {} + ord => return ord, + } + self.payload.cmp(&other.payload) + } +} + +impl PartialOrd for ActionV1 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl AgentOutput { + /// Canonicalize actions by sorting them into deterministic order. + /// + /// NOTE: The `encode()` method automatically canonicalizes actions, + /// so calling this explicitly is only needed if you want to inspect + /// the canonical order without encoding. + pub fn canonicalize(&mut self) { + self.actions.sort(); + } + + /// Return a new AgentOutput with canonicalized action order. + /// + /// NOTE: The `encode()` method automatically canonicalizes actions, + /// so calling this explicitly is only needed if you want to inspect + /// the canonical order without encoding. + pub fn into_canonical(mut self) -> Self { + self.canonicalize(); + self + } +} + #[derive(Clone, Debug, PartialEq)] pub enum CodecError { InvalidLength, @@ -119,6 +176,7 @@ pub enum CodecError { ArithmeticOverflow, TooManyActions { count: u32, limit: usize }, ActionPayloadTooLarge { size: u32, limit: usize }, + ActionTooLarge { size: u32, limit: usize }, } /// Kernel-level execution errors. @@ -142,6 +200,8 @@ pub enum KernelError { InvalidAgentId, /// Agent code hash mismatch AgentCodeHashMismatch, + /// Output encoding failed + EncodingFailed(CodecError), } /// Agent execution errors diff --git a/crates/kernel-guest/src/lib.rs b/crates/kernel-guest/src/lib.rs index ffcaf23..5aa94ac 100644 --- a/crates/kernel-guest/src/lib.rs +++ b/crates/kernel-guest/src/lib.rs @@ -79,8 +79,9 @@ pub fn kernel_main(input_bytes: &[u8]) -> Result, KernelError> { check(&agent_output, &constraint_meta) .map_err(KernelError::ConstraintViolation)?; - // 9. Compute action commitment - let agent_output_bytes = agent_output.encode(); + // 9. Compute action commitment (encode() automatically canonicalizes) + let agent_output_bytes = agent_output.encode() + .map_err(KernelError::EncodingFailed)?; let action_commitment = compute_action_commitment(&agent_output_bytes); // 10. Construct journal with all identity and commitment fields @@ -97,5 +98,5 @@ pub fn kernel_main(input_bytes: &[u8]) -> Result, KernelError> { execution_status: ExecutionStatus::Success, }; - Ok(journal.encode()) + journal.encode().map_err(KernelError::EncodingFailed) } From cb439b59c4f3e5dfcd8613385b36e55175429e2f Mon Sep 17 00:00:00 2001 From: mehdi-defiesta Date: Wed, 21 Jan 2026 19:58:03 +0100 Subject: [PATCH 3/3] documentaiton --- README.md | 8 +-- SPECIFICATION.md | 87 --------------------------- docs/P0_DOCUMENTATION.md | 126 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 92 deletions(-) delete mode 100644 SPECIFICATION.md create mode 100644 docs/P0_DOCUMENTATION.md diff --git a/README.md b/README.md index 587e304..7e25b24 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,14 @@ # Execution Kernel - P0.1 Canonical zkVM Guest Program [![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](#testing) -[![Tests](https://img.shields.io/badge/tests-11%20passed-brightgreen)](#testing) +[![Tests](https://img.shields.io/badge/tests-%20passed-brightgreen)](#testing) [![Deterministic](https://img.shields.io/badge/execution-deterministic-blue)](#consensus-critical-properties) ## Overview -This repository implements the **P0.1 Canonical zkVM Guest Program** as specified in `SPECIFICATION.md`. The kernel provides consensus-critical, deterministic agent execution within a RISC Zero zkVM environment. +This repository implements the **P0.1 Canonical zkVM Guest Program**. The kernel provides consensus-critical, deterministic agent execution within a RISC Zero zkVM environment. -**🎯 Purpose:** Define what constitutes a valid agent execution through cryptographically verifiable zero-knowledge proofs. - -**⚡ Status:** ✅ **Implementation Complete** - All P0.1 requirements fulfilled with comprehensive test coverage. +**Purpose:** Define what constitutes a valid agent execution through cryptographically verifiable zero-knowledge proofs. ## Architecture diff --git a/SPECIFICATION.md b/SPECIFICATION.md deleted file mode 100644 index a230b02..0000000 --- a/SPECIFICATION.md +++ /dev/null @@ -1,87 +0,0 @@ -# SPECIFICATION.md -## P0.1 — Canonical zkVM Guest Program (Execution Kernel) - -**Project:** Verifiable Agent Execution Kernel -**Milestone:** Q1 2026 — Priority 0 (Blocking) -**Task:** P0.1 Canonical zkVM Guest Program -**Status:** Specification v1.0 (Implementation-Ready) -**Target VM:** RISC Zero zkVM -**Last Updated:** 2026-01-20 - ---- - -## 1. Purpose - -This document specifies the **canonical zkVM guest program** (“Execution Kernel”) that defines what it means for an agent execution to be **valid**. - -The kernel is responsible for: - -1. Deterministically executing agent logic -2. Enforcing non-bypassable constraints -3. Producing a canonical journal committed to a zk receipt - -**Protocol invariant:** -> Any receipt accepted on-chain MUST correspond to an execution of this kernel with identical semantics. - -The kernel is consensus-critical. Any ambiguity breaks the protocol. - ---- - -## 2. Non-Goals - -The kernel explicitly does NOT: - -- Fetch or validate external data -- Interpret market semantics -- Perform settlement or state transitions -- Manage vault balances or capital -- Enforce economic policy beyond calling the constraint engine - -The kernel proves **correct execution given committed inputs**, nothing more. - ---- - -## 3. Threat Model - -Assume a malicious operator who: - -- Controls the host environment -- Attempts to forge agent outputs -- Attempts to bypass constraints -- Attempts to exploit non-determinism - -The kernel MUST ensure such attempts fail cryptographically. - ---- - -## 4. Determinism Requirements (Hard MUSTs) - -The kernel MUST be fully deterministic across: - -- Machines -- Provers -- Rebuilds (with pinned toolchain) - -### 4.1 Forbidden Sources of Non-Determinism - -The guest MUST NOT depend on: - -- System time -- Randomness -- Floating point operations -- Host environment variables -- Filesystem or network access -- Unordered iteration without canonical sorting - -All loops MUST be bounded. -All memory usage MUST be bounded. - ---- - -## 5. Versioning and Constants (v1) - -```text -PROTOCOL_VERSION = 1 -KERNEL_VERSION = 1 -MAX_AGENT_INPUT_BYTES = 64_000 -HASH_FUNCTION = SHA-256 \ No newline at end of file diff --git a/docs/P0_DOCUMENTATION.md b/docs/P0_DOCUMENTATION.md new file mode 100644 index 0000000..6fe96cc --- /dev/null +++ b/docs/P0_DOCUMENTATION.md @@ -0,0 +1,126 @@ +# Execution Kernel — Developer Documentation (P0.1) + +## Introduction + +The execution kernel is the core of the protocol. It is the component that defines what it means for an off-chain agent execution to be considered valid, provable, and safe to settle on-chain. Everything else in the system exists to support, invoke, or verify the kernel. If the kernel behaves correctly, the protocol works. If it does not, no amount of surrounding infrastructure can compensate for it. + +As a developer, you should think of the kernel as consensus code rather than application code. Any change that could cause two honest provers to produce different output bytes for the same input is a protocol change. This document explains how the kernel works, why it is structured the way it is, and how you are expected to use it. + + + +## What the kernel does + +The kernel takes a fully specified input, executes an agent deterministically inside a zkVM, enforces protocol constraints, and produces a cryptographic journal. That journal is the only artifact that leaves the zkVM and is the only thing that on-chain contracts or off-chain verifiers need to interpret. + +From a usage perspective, the kernel answers one question and one question only: given these exact input bytes, did this exact agent code execute correctly under these exact rules, and if so, what actions did it produce? + + + +## Determinism and canonical behavior + +Determinism is the most important property of the kernel. Given the same input bytes, the kernel must always follow the same execution path and must always produce the same output bytes. This must hold across different machines, different provers, and different builds, as long as the toolchain is pinned. + +Canonical behavior is how determinism is enforced. For every protocol-relevant concept, such as inputs, outputs, ordering, and hashing, the kernel defines exactly one valid representation. There are no alternative encodings, no equivalent orderings, and no tolerated ambiguities. If two implementations disagree on a single byte, they are not both correct. + + + +## Kernel input and how to construct it + +The kernel input is not just a set of parameters. It is a binding statement that defines exactly what is being proven. When you construct a kernel input, you are committing to a specific protocol version, a specific kernel implementation, a specific agent and agent binary, a specific constraint policy, a specific external state snapshot, and a specific execution instance. + +The input is represented by the KernelInputV1 structure. It includes version fields, identifiers, cryptographic hashes, a replay-protection nonce, and an opaque byte array that is passed verbatim to the agent. The kernel does not interpret the opaque agent input. It only enforces size limits and canonical encoding rules. + +When encoding a kernel input, you must follow the canonical little-endian encoding defined by the kernel. Length prefixes are mandatory, size limits are enforced, and any extra or trailing bytes will cause decoding to fail. If decoding fails inside the kernel, execution aborts and no proof can be produced. + + + +## Input commitment and why it matters + +After decoding the input, the kernel computes an input commitment by hashing the full input byte sequence with SHA-256. This hash is taken over the exact bytes that were provided to the kernel, without re-encoding or normalization. + +This commitment ensures that the proof is bound to the precise input that was executed. It prevents operators from reinterpreting inputs after the fact and allows on-chain logic to reason about what the agent actually observed. When you use the kernel, you should always treat the input commitment as the authoritative fingerprint of the execution context. + + +## Agent execution model + +Agents are executed as pure, deterministic functions. An agent receives an execution context and a byte array of opaque inputs and produces a structured output. Agents have no access to time, randomness, I/O, or any external state beyond what is explicitly provided to them. + +The kernel always calls the agent. The agent never calls back into the kernel. This asymmetry is intentional and security-critical. It ensures that agents cannot bypass constraint checks, cannot influence commitments, and cannot affect encoding or ordering rules. + +From a developer perspective, writing an agent means implementing logic that proposes actions, not logic that enforces safety. Safety is always the kernel’s responsibility. + + +## Agent output and action structure + +An agent produces an AgentOutput, which is a collection of structured actions. Each action includes an action type identifier, a target identifier, and an opaque payload. The kernel enforces strict limits on the number of actions and the size of each payload. + +These limits are enforced both when encoding and when validating the output structure. Even if an agent attempts to produce an oversized or malformed output, the kernel will detect it and abort execution. + + +## Canonical action ordering + +Agents are not trusted to produce actions in a consistent or meaningful order. To avoid ambiguity, the kernel enforces canonical ordering during encoding. Actions are sorted deterministically by action type, then by target, and finally by payload bytes. + +This sorting happens inside the encoding logic itself. As a result, it is impossible to accidentally compute a commitment over a non-canonical ordering. This guarantees that all honest provers will compute identical action commitments, even if agents emit actions in arbitrary order. + +As a user of the kernel, you should assume that action order is normalized and should never rely on the original order in which an agent produced actions. + + +## Constraint enforcement + +Constraint enforcement is mandatory. Every kernel execution validates the agent output structure and then applies the constraint policy associated with the provided constraint set hash. + +In P0.1, the constraint logic is intentionally a stub that always succeeds. However, the call is still required and enforced. This locks in the protocol structure and ensures that future versions can introduce real constraints without changing the execution flow. + +If any constraint check fails, execution aborts immediately and no journal is produced. + + +## Action commitment + +After canonicalizing and encoding the agent output, the kernel computes an action commitment by hashing the encoded bytes with SHA-256. This commitment binds the proof to the exact set of actions produced by the agent under canonical ordering. + +On-chain settlement logic should treat the action commitment as the authoritative representation of what the agent decided to do. + + +## Kernel journal and how to use it + +The kernel journal is the only output of a successful execution. It is a fixed-size, deterministic structure that contains all information needed for verification and settlement. It includes version fields, identity bindings, the execution nonce, the input commitment, the action commitment, and a success status. + +The journal is committed to the zkVM environment only if execution succeeds. If execution fails for any reason, no journal bytes are committed and no valid proof can exist. + +When you build on top of the kernel, your on-chain or off-chain verifier should decode the journal, validate its versions, and use its commitments to authorize or reject actions. + + +## Failure semantics + +The kernel follows a single, simple rule for failures. Any error results in no journal. There are no partial results, no failure receipts, and no ambiguous states. + +This applies to decoding errors, version mismatches, agent execution failures, constraint violations, and encoding errors. In the zkVM guest, failures cause a panic, which prevents any journal from being committed. + + +## zkVM integration + +The kernel itself is independent of the zkVM. It operates on raw byte slices and returns raw byte vectors. The zkVM guest wrapper is intentionally thin and exists only to read input bytes, invoke the kernel, and commit the resulting journal bytes. + +As a developer, you should avoid placing logic in the guest wrapper. All protocol semantics must live in the kernel core. + + +## Versioning and upgrades + +The kernel tracks both a protocol version and a kernel version. The protocol version governs wire format compatibility, while the kernel version governs execution semantics. Any change to encoding rules, execution logic, or ordering rules requires a kernel version bump. + +Old versions are never reinterpreted. A verifier must always know exactly which kernel semantics a proof corresponds to. + + +## How to use the kernel correctly + +To use the kernel correctly, you should construct a canonical KernelInputV1, encode it using the provided encoding rules, and pass the resulting bytes to the zkVM guest. You should never attempt to partially construct journals or commitments yourself. All commitments must be computed by the kernel. + +On the verification side, you should treat the kernel journal as the single source of truth. Never trust off-chain claims about what an agent did without validating the journal and its commitments. + + +## Final note for developers + +When working on this codebase, always ask yourself whether a change could affect determinism, encoding, ordering, or commitment computation. If the answer is yes, the change is protocol-critical and must be treated as such. + +The kernel is not just another library. It is the definition of the protocol itself. \ No newline at end of file