diff --git a/Cargo.lock b/Cargo.lock index 2126486..09e4cfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,8 +78,16 @@ dependencies = [ "constraints", "kernel-core", "kernel-guest", + "serde", + "serde_json", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "kernel-core" version = "0.1.0" @@ -102,6 +110,73 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha2" version = "0.10.9" @@ -113,14 +188,37 @@ dependencies = [ "digest", ] +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/crates/host-tests/Cargo.toml b/crates/host-tests/Cargo.toml index 6ff00f6..97b1236 100644 --- a/crates/host-tests/Cargo.toml +++ b/crates/host-tests/Cargo.toml @@ -7,4 +7,8 @@ edition = "2021" kernel-core = { path = "../kernel-core" } kernel-guest = { path = "../kernel-guest" } agent-traits = { path = "../agent-traits" } -constraints = { path = "../constraints" } \ No newline at end of file +constraints = { path = "../constraints" } + +[dev-dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/crates/host-tests/src/lib.rs b/crates/host-tests/src/lib.rs index 7a5bfe4..35258b7 100644 --- a/crates/host-tests/src/lib.rs +++ b/crates/host-tests/src/lib.rs @@ -1,6 +1,11 @@ #[cfg(test)] mod tests { use kernel_core::*; + use kernel_core::codec::{ + put_u32_le, put_u64_le, put_bytes32, + get_u32_le, get_u64_le, get_bytes32, + ensure_no_trailing_bytes, + }; use kernel_guest::kernel_main; /// Helper to create a valid KernelInputV1 with default values @@ -487,4 +492,382 @@ mod tests { // Fixed fields (144) + length prefix (4) + 0 bytes data = 148 assert_eq!(encoded.len(), 148); } + + // ======================================================================== + // P0.2: Trailing Bytes Rejection Tests + // ======================================================================== + + #[test] + fn test_trailing_bytes_rejected_input() { + // Create a valid KernelInputV1 encoding + let input = KernelInputV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0u8; 32], + agent_code_hash: [0u8; 32], + constraint_set_hash: [0u8; 32], + input_root: [0u8; 32], + execution_nonce: 0, + opaque_agent_inputs: vec![], + }; + let mut encoded = input.encode().unwrap(); + + // Append trailing byte + encoded.push(0xFF); + + // Decode should fail with InvalidLength + let result = KernelInputV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidLength)), + "Expected InvalidLength error for trailing bytes, got {:?}", result); + } + + #[test] + fn test_trailing_bytes_rejected_journal() { + // Create a valid KernelJournalV1 encoding + let journal = KernelJournalV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0u8; 32], + agent_code_hash: [0u8; 32], + constraint_set_hash: [0u8; 32], + input_root: [0u8; 32], + execution_nonce: 0, + input_commitment: [0u8; 32], + action_commitment: [0u8; 32], + execution_status: ExecutionStatus::Success, + }; + let mut encoded = journal.encode().unwrap(); + assert_eq!(encoded.len(), 209); // Fixed size + + // Append trailing byte + encoded.push(0xFF); + assert_eq!(encoded.len(), 210); + + // Decode should fail with InvalidLength + let result = KernelJournalV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidLength)), + "Expected InvalidLength error for trailing bytes, got {:?}", result); + } + + #[test] + fn test_trailing_bytes_rejected_action() { + // Create a valid ActionV1 encoding + let action = ActionV1 { + action_type: 1, + target: [0x42u8; 32], + payload: vec![1, 2, 3], + }; + let mut encoded = action.encode().unwrap(); + + // Append trailing byte + encoded.push(0xFF); + + // Decode should fail with InvalidLength + let result = ActionV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidLength)), + "Expected InvalidLength error for trailing bytes, got {:?}", result); + } + + #[test] + fn test_trailing_bytes_rejected_agent_output() { + // Create a valid AgentOutput encoding with one action + let output = AgentOutput { + actions: vec![ActionV1 { + action_type: 1, + target: [0x42u8; 32], + payload: vec![], + }], + }; + let mut encoded = output.encode().unwrap(); + + // Append trailing byte + encoded.push(0xFF); + + // Decode should fail with InvalidLength + let result = AgentOutput::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidLength)), + "Expected InvalidLength error for trailing bytes, got {:?}", result); + } + + // ======================================================================== + // P0.2: Commitment Helper Tests + // ======================================================================== + + #[test] + fn test_kernel_input_v1_commitment_helper() { + // Create input and compute commitment using helper + let input = KernelInputV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0u8; 32], + agent_code_hash: [0u8; 32], + constraint_set_hash: [0u8; 32], + input_root: [0u8; 32], + execution_nonce: 0, + opaque_agent_inputs: vec![], + }; + + // Using helper function + let commitment_via_helper = kernel_input_v1_commitment(&input).unwrap(); + + // Manual computation (encode then hash) + let encoded = input.encode().unwrap(); + let commitment_manual = compute_input_commitment(&encoded); + + // Both should produce the same result + assert_eq!(commitment_via_helper, commitment_manual); + + // Verify known golden value + let expected_hex = "f0b4a449964d5ff3e473605e3ed1af1223f60135392d8add3244d2926ab9ab3f"; + let expected: [u8; 32] = hex_to_bytes32(expected_hex); + assert_eq!(commitment_via_helper, expected); + } + + #[test] + fn test_kernel_input_v1_commitment_helper_standard_case() { + let input = KernelInputV1 { + protocol_version: PROTOCOL_VERSION, + kernel_version: KERNEL_VERSION, + agent_id: [0x42u8; 32], + agent_code_hash: [0xaau8; 32], + constraint_set_hash: [0xbbu8; 32], + input_root: [0xccu8; 32], + execution_nonce: 12345, + opaque_agent_inputs: vec![1, 2, 3, 4, 5], + }; + + let commitment = kernel_input_v1_commitment(&input).unwrap(); + + // Verify known golden value + let expected_hex = "6e4a2cce578937164ab4c0016a678b8e9d24a729c7c418b793b447fd299ff6a4"; + let expected: [u8; 32] = hex_to_bytes32(expected_hex); + assert_eq!(commitment, expected); + } + + // ======================================================================== + // P0.2: Golden Vector Tests + // ======================================================================== + + #[test] + fn test_golden_vector_kernel_input_minimal() { + // Golden vector: minimal_zeros + let encoded_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + let commitment_hex = "f0b4a449964d5ff3e473605e3ed1af1223f60135392d8add3244d2926ab9ab3f"; + + let encoded = hex_to_vec(encoded_hex); + let expected_commitment = hex_to_bytes32(commitment_hex); + + // Decode and verify + let decoded = KernelInputV1::decode(&encoded).unwrap(); + assert_eq!(decoded.protocol_version, 1); + assert_eq!(decoded.kernel_version, 1); + assert_eq!(decoded.agent_id, [0u8; 32]); + assert_eq!(decoded.execution_nonce, 0); + assert!(decoded.opaque_agent_inputs.is_empty()); + + // Re-encode and verify round-trip + let re_encoded = decoded.encode().unwrap(); + assert_eq!(re_encoded, encoded); + + // Verify commitment + let commitment = compute_input_commitment(&encoded); + assert_eq!(commitment, expected_commitment); + } + + #[test] + fn test_golden_vector_kernel_input_standard() { + // Golden vector: standard_case + let encoded_hex = "01000000010000004242424242424242424242424242424242424242424242424242424242424242aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc3930000000000000050000000102030405"; + let commitment_hex = "6e4a2cce578937164ab4c0016a678b8e9d24a729c7c418b793b447fd299ff6a4"; + + let encoded = hex_to_vec(encoded_hex); + let expected_commitment = hex_to_bytes32(commitment_hex); + + // Decode and verify + let decoded = KernelInputV1::decode(&encoded).unwrap(); + assert_eq!(decoded.protocol_version, 1); + assert_eq!(decoded.kernel_version, 1); + assert_eq!(decoded.agent_id, [0x42u8; 32]); + assert_eq!(decoded.agent_code_hash, [0xaau8; 32]); + assert_eq!(decoded.execution_nonce, 12345); + assert_eq!(decoded.opaque_agent_inputs, vec![1, 2, 3, 4, 5]); + + // Re-encode and verify round-trip + let re_encoded = decoded.encode().unwrap(); + assert_eq!(re_encoded, encoded); + + // Verify commitment + let commitment = compute_input_commitment(&encoded); + assert_eq!(commitment, expected_commitment); + } + + #[test] + fn test_golden_vector_kernel_journal_minimal() { + // Golden vector: journal minimal_zeros (209 bytes) + let encoded_hex = "0100000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"; + + let encoded = hex_to_vec(encoded_hex); + assert_eq!(encoded.len(), 209); // Fixed size + + // Decode and verify + let decoded = KernelJournalV1::decode(&encoded).unwrap(); + assert_eq!(decoded.protocol_version, 1); + assert_eq!(decoded.kernel_version, 1); + assert_eq!(decoded.agent_id, [0u8; 32]); + assert_eq!(decoded.execution_status, ExecutionStatus::Success); + + // Re-encode and verify round-trip + let re_encoded = decoded.encode().unwrap(); + assert_eq!(re_encoded, encoded); + } + + #[test] + fn test_golden_vector_kernel_journal_standard() { + // Golden vector: journal standard_case (209 bytes) + let encoded_hex = "01000000010000004242424242424242424242424242424242424242424242424242424242424242aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc3930000000000000ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01"; + + let encoded = hex_to_vec(encoded_hex); + assert_eq!(encoded.len(), 209); + + // Decode and verify + let decoded = KernelJournalV1::decode(&encoded).unwrap(); + assert_eq!(decoded.protocol_version, 1); + assert_eq!(decoded.agent_id, [0x42u8; 32]); + assert_eq!(decoded.agent_code_hash, [0xaau8; 32]); + assert_eq!(decoded.execution_nonce, 12345); + assert_eq!(decoded.input_commitment, [0xddu8; 32]); + assert_eq!(decoded.action_commitment, [0xeeu8; 32]); + assert_eq!(decoded.execution_status, ExecutionStatus::Success); + + // Re-encode and verify round-trip + let re_encoded = decoded.encode().unwrap(); + assert_eq!(re_encoded, encoded); + } + + // ======================================================================== + // P0.2: Negative Vector Tests + // ======================================================================== + + #[test] + fn test_negative_vector_truncated() { + // Truncated input (only 2 bytes) + let encoded = hex_to_vec("0102"); + let result = KernelInputV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::UnexpectedEndOfInput))); + } + + #[test] + fn test_negative_vector_trailing_bytes() { + // Valid encoding with extra trailing byte 0xff + let encoded_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff"; + let encoded = hex_to_vec(encoded_hex); + + let result = KernelInputV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidLength))); + } + + #[test] + fn test_negative_vector_wrong_version() { + // Protocol version set to 999 (0xe7030000 in little-endian) + let encoded_hex = "e7030000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + let encoded = hex_to_vec(encoded_hex); + + let result = KernelInputV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::InvalidVersion { expected: 1, actual: 999 }))); + } + + #[test] + fn test_negative_vector_empty_input() { + // Empty byte array + let encoded: Vec = vec![]; + let result = KernelInputV1::decode(&encoded); + assert!(matches!(result, Err(CodecError::UnexpectedEndOfInput))); + } + + // ======================================================================== + // P0.2: Helper Function Tests + // ======================================================================== + + #[test] + fn test_codec_helper_put_get_u32() { + let mut buf = Vec::new(); + put_u32_le(&mut buf, 0x12345678); + assert_eq!(buf, vec![0x78, 0x56, 0x34, 0x12]); + + let mut offset = 0; + let value = get_u32_le(&buf, &mut offset).unwrap(); + assert_eq!(value, 0x12345678); + assert_eq!(offset, 4); + } + + #[test] + fn test_codec_helper_put_get_u64() { + let mut buf = Vec::new(); + put_u64_le(&mut buf, 0x123456789ABCDEF0); + assert_eq!(buf, vec![0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12]); + + let mut offset = 0; + let value = get_u64_le(&buf, &mut offset).unwrap(); + assert_eq!(value, 0x123456789ABCDEF0); + assert_eq!(offset, 8); + } + + #[test] + fn test_codec_helper_put_get_bytes32() { + let bytes: [u8; 32] = [0x42; 32]; + let mut buf = Vec::new(); + put_bytes32(&mut buf, &bytes); + assert_eq!(buf.len(), 32); + + let mut offset = 0; + let result = get_bytes32(&buf, &mut offset).unwrap(); + assert_eq!(result, bytes); + assert_eq!(offset, 32); + } + + #[test] + fn test_codec_helper_ensure_no_trailing_bytes() { + // Exact match - should succeed + let result = ensure_no_trailing_bytes(&[1, 2, 3], 3); + assert!(result.is_ok()); + + // Trailing bytes - should fail + let result = ensure_no_trailing_bytes(&[1, 2, 3, 4], 3); + assert!(matches!(result, Err(CodecError::InvalidLength))); + } + + #[test] + fn test_codec_helper_get_u32_insufficient_bytes() { + let buf = vec![0x01, 0x02]; // Only 2 bytes + let mut offset = 0; + let result = get_u32_le(&buf, &mut offset); + assert!(matches!(result, Err(CodecError::UnexpectedEndOfInput))); + } + + #[test] + fn test_codec_helper_get_bytes32_insufficient_bytes() { + let buf = vec![0x42; 16]; // Only 16 bytes + let mut offset = 0; + let result = get_bytes32(&buf, &mut offset); + assert!(matches!(result, Err(CodecError::UnexpectedEndOfInput))); + } + + // ======================================================================== + // Test Helpers + // ======================================================================== + + fn hex_to_vec(hex: &str) -> Vec { + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()) + .collect() + } + + fn hex_to_bytes32(hex: &str) -> [u8; 32] { + let vec = hex_to_vec(hex); + assert_eq!(vec.len(), 32, "Expected 32 bytes, got {}", vec.len()); + let mut arr = [0u8; 32]; + arr.copy_from_slice(&vec); + arr + } } diff --git a/crates/kernel-core/src/codec.rs b/crates/kernel-core/src/codec.rs index 0fae9f9..8654bd5 100644 --- a/crates/kernel-core/src/codec.rs +++ b/crates/kernel-core/src/codec.rs @@ -1,6 +1,138 @@ +// Canonical binary codec for P0.1 kernel protocol. +// See spec/codec.md for encoding specification. + use crate::types::*; use crate::{MAX_AGENT_INPUT_BYTES, PROTOCOL_VERSION, KERNEL_VERSION}; +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Encode a u32 as little-endian bytes and append to buffer. +#[inline] +pub fn put_u32_le(buf: &mut Vec, value: u32) { + buf.extend_from_slice(&value.to_le_bytes()); +} + +/// Encode a u64 as little-endian bytes and append to buffer. +#[inline] +pub fn put_u64_le(buf: &mut Vec, value: u64) { + buf.extend_from_slice(&value.to_le_bytes()); +} + +/// Append a 32-byte array to buffer. +#[inline] +pub fn put_bytes32(buf: &mut Vec, bytes: &[u8; 32]) { + buf.extend_from_slice(bytes); +} + +/// Encode variable-length bytes with u32 length prefix. +/// Returns error if data exceeds max_len. +#[inline] +pub fn put_var_bytes(buf: &mut Vec, data: &[u8], max_len: usize) -> Result<(), CodecError> { + let len = data.len(); + if len > max_len { + return Err(CodecError::InputTooLarge { + size: len.min(u32::MAX as usize) as u32, + limit: max_len, + }); + } + if len > u32::MAX as usize { + return Err(CodecError::ArithmeticOverflow); + } + buf.extend_from_slice(&(len as u32).to_le_bytes()); + buf.extend_from_slice(data); + Ok(()) +} + +/// Decode a u32 from little-endian bytes at offset. +/// Advances offset by 4 on success. +#[inline] +pub fn get_u32_le(bytes: &[u8], offset: &mut usize) -> Result { + if bytes.len() < *offset + 4 { + return Err(CodecError::UnexpectedEndOfInput); + } + let value = u32::from_le_bytes( + bytes[*offset..*offset + 4] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)? + ); + *offset += 4; + Ok(value) +} + +/// Decode a u64 from little-endian bytes at offset. +/// Advances offset by 8 on success. +#[inline] +pub fn get_u64_le(bytes: &[u8], offset: &mut usize) -> Result { + if bytes.len() < *offset + 8 { + return Err(CodecError::UnexpectedEndOfInput); + } + let value = u64::from_le_bytes( + bytes[*offset..*offset + 8] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)? + ); + *offset += 8; + Ok(value) +} + +/// Decode a 32-byte array at offset. +/// Advances offset by 32 on success. +#[inline] +pub fn get_bytes32(bytes: &[u8], offset: &mut usize) -> Result<[u8; 32], CodecError> { + if bytes.len() < *offset + 32 { + return Err(CodecError::UnexpectedEndOfInput); + } + let value: [u8; 32] = bytes[*offset..*offset + 32] + .try_into() + .map_err(|_| CodecError::UnexpectedEndOfInput)?; + *offset += 32; + Ok(value) +} + +/// Decode variable-length bytes with u32 length prefix. +/// Advances offset by 4 + length on success. +/// Returns error if length exceeds max_len. +#[inline] +pub fn get_var_bytes(bytes: &[u8], offset: &mut usize, max_len: usize) -> Result, CodecError> { + let len_u32 = get_u32_le(bytes, offset)?; + // Rewind offset since get_u32_le advanced it, but we need to check bounds + *offset -= 4; + + if len_u32 > max_len as u32 { + return Err(CodecError::InputTooLarge { + size: len_u32, + limit: max_len, + }); + } + + let len = len_u32 as usize; + *offset += 4; // Re-advance past length + + if bytes.len() < *offset + len { + return Err(CodecError::UnexpectedEndOfInput); + } + + let data = bytes[*offset..*offset + len].to_vec(); + *offset += len; + Ok(data) +} + +/// Ensure there are no trailing bytes after decoding. +/// Returns error if offset does not equal total length. +#[inline] +pub fn ensure_no_trailing_bytes(bytes: &[u8], offset: usize) -> Result<(), CodecError> { + if offset != bytes.len() { + return Err(CodecError::InvalidLength); + } + Ok(()) +} + +// ============================================================================ +// Traits +// ============================================================================ + pub trait CanonicalEncode { fn encode(&self) -> Result, CodecError>; } diff --git a/crates/kernel-core/src/hash.rs b/crates/kernel-core/src/hash.rs index 4caad2b..ba8efa6 100644 --- a/crates/kernel-core/src/hash.rs +++ b/crates/kernel-core/src/hash.rs @@ -1,4 +1,6 @@ use sha2::{Sha256, Digest}; +use crate::types::{KernelInputV1, CodecError}; +use crate::codec::CanonicalEncode; pub fn compute_input_commitment(input_bytes: &[u8]) -> [u8; 32] { let mut hasher = Sha256::new(); @@ -10,4 +12,13 @@ pub fn compute_action_commitment(agent_output_bytes: &[u8]) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(agent_output_bytes); hasher.finalize().into() +} + +/// Compute SHA-256 commitment for a KernelInputV1 structure. +/// +/// This is a convenience function that encodes the input and computes +/// the commitment in one step. Useful for tests and external tooling. +pub fn kernel_input_v1_commitment(input: &KernelInputV1) -> Result<[u8; 32], CodecError> { + let bytes = input.encode()?; + Ok(compute_input_commitment(&bytes)) } \ No newline at end of file diff --git a/spec/codec.md b/spec/codec.md new file mode 100644 index 0000000..5e8483f --- /dev/null +++ b/spec/codec.md @@ -0,0 +1,285 @@ +# Canonical Codec Specification + +This document specifies the canonical binary encoding for the kernel protocol (version 1). +All encodings are deterministic and consensus-critical. + +## Design Principles + +1. **Determinism**: Identical data structures always produce identical byte sequences +2. **Self-describing lengths**: Variable-length fields are prefixed with their byte length +3. **Little-endian integers**: All multi-byte integers use little-endian byte order +4. **No implicit padding**: Structures are tightly packed with no alignment padding +5. **Strict decoding**: Trailing bytes, invalid versions, and out-of-range values are rejected + +--- + +## Primitive Encoding Rules + +### Integers + +| Type | Encoding | Size | +|------|----------|------| +| `u32` | Little-endian | 4 bytes | +| `u64` | Little-endian | 8 bytes | +| `u8` | Raw byte | 1 byte | + +### Fixed-Size Byte Arrays + +| Type | Encoding | Size | +|------|----------|------| +| `[u8; 32]` | Raw bytes (no length prefix) | 32 bytes | + +### Variable-Length Byte Arrays + +| Type | Encoding | Size | +|------|----------|------| +| `Vec` | `[length: u32][data: u8*]` | 4 + length bytes | + +The `length` field specifies the **number of bytes** in the data that follows. + +--- + +## KernelInputV1 + +### Layout + +| Offset | Field | Type | Size | Description | +|--------|-------|------|------|-------------| +| 0 | `protocol_version` | u32 | 4 | Wire format version | +| 4 | `kernel_version` | u32 | 4 | Kernel semantics version | +| 8 | `agent_id` | [u8; 32] | 32 | Agent identifier | +| 40 | `agent_code_hash` | [u8; 32] | 32 | SHA-256 of agent binary | +| 72 | `constraint_set_hash` | [u8; 32] | 32 | SHA-256 of constraint set | +| 104 | `input_root` | [u8; 32] | 32 | External state root (market/vault snapshot) | +| 136 | `execution_nonce` | u64 | 8 | Replay protection nonce | +| 144 | `opaque_agent_inputs_len` | u32 | 4 | Length of agent input data (bytes) | +| 148 | `opaque_agent_inputs` | [u8; *] | variable | Agent-specific input (max 64,000 bytes) | + +### Size + +- **Fixed header**: 148 bytes +- **Total**: 148 + `opaque_agent_inputs_len` bytes +- **Minimum**: 148 bytes (empty input) +- **Maximum**: 148 + 64,000 = 64,148 bytes + +### Validation Rules + +1. `protocol_version` MUST equal `PROTOCOL_VERSION` (currently 1) +2. `kernel_version` MUST equal `KERNEL_VERSION` (currently 1) +3. `opaque_agent_inputs_len` MUST NOT exceed `MAX_AGENT_INPUT_BYTES` (64,000) +4. Total bytes MUST equal exactly 148 + `opaque_agent_inputs_len` (no trailing bytes) +5. Decoders MUST reject if `148 + opaque_agent_inputs_len` would overflow + +--- + +## KernelJournalV1 + +### Layout + +| Offset | Field | Type | Size | Description | +|--------|-------|------|------|-------------| +| 0 | `protocol_version` | u32 | 4 | Wire format version | +| 4 | `kernel_version` | u32 | 4 | Kernel semantics version | +| 8 | `agent_id` | [u8; 32] | 32 | Agent identifier (copied from input) | +| 40 | `agent_code_hash` | [u8; 32] | 32 | Agent code hash (copied from input) | +| 72 | `constraint_set_hash` | [u8; 32] | 32 | Constraint set hash (copied from input) | +| 104 | `input_root` | [u8; 32] | 32 | External state root (copied from input) | +| 136 | `execution_nonce` | u64 | 8 | Execution nonce (copied from input) | +| 144 | `input_commitment` | [u8; 32] | 32 | SHA-256 of encoded KernelInputV1 | +| 176 | `action_commitment` | [u8; 32] | 32 | SHA-256 of encoded AgentOutput | +| 208 | `execution_status` | u8 | 1 | Execution result | + +### Size + +- **Fixed**: 209 bytes (always) + +### Validation Rules + +1. `protocol_version` MUST equal `PROTOCOL_VERSION` (currently 1) +2. `kernel_version` MUST equal `KERNEL_VERSION` (currently 1) +3. `execution_status` MUST equal 0x01 (Success) +4. Total bytes MUST equal exactly 209 (no more, no less) + +--- + +## ExecutionStatus + +### Encoding + +| Value | Status | Description | +|-------|--------|-------------| +| 0x00 | Reserved | Invalid (catches uninitialized memory) | +| 0x01 | Success | Execution completed successfully | +| 0x02-0xFF | Reserved | Invalid (reserved for future expansion) | + +### Rationale + +In protocol version 1, the journal is **only published for successful execution**. Failures cause the kernel to abort before committing a journal. Therefore, only 0x01 (Success) is a valid value. The value 0x00 is explicitly reserved to detect bugs where uninitialized memory is accidentally interpreted as a valid status. + +Decoders MUST reject any value other than 0x01. + +--- + +## ActionV1 + +### Layout + +| Offset | Field | Type | Size | Description | +|--------|-------|------|------|-------------| +| 0 | `action_type` | u32 | 4 | Action type identifier | +| 4 | `target` | [u8; 32] | 32 | Target address/identifier | +| 36 | `payload_len` | u32 | 4 | Length of payload data (bytes) | +| 40 | `payload` | [u8; *] | variable | Action-specific data (max 16,384 bytes) | + +### Size + +- **Fixed header**: 40 bytes +- **Total**: 40 + `payload_len` bytes +- **Minimum**: 40 bytes (empty payload) +- **Maximum**: 40 + 16,384 = 16,424 bytes (`MAX_SINGLE_ACTION_BYTES`) + +### Validation Rules + +1. `payload_len` MUST NOT exceed `MAX_ACTION_PAYLOAD_BYTES` (16,384) +2. Total bytes MUST equal exactly 40 + `payload_len` (no trailing bytes) +3. When embedded in AgentOutput, the `action_len` prefix MUST exactly equal the actual byte length of the ActionV1 encoding (i.e., `action_len == 40 + payload_len`) + +--- + +## AgentOutput + +### Layout + +| Offset | Field | Type | Size | Description | +|--------|-------|------|------|-------------| +| 0 | `action_count` | u32 | 4 | Number of actions | +| 4 | actions[0..n] | ActionV1[] | variable | Length-prefixed actions | + +Each action is encoded as: +- `action_len: u32` (4 bytes) - Byte length of the following ActionV1 encoding +- `action: ActionV1` (variable) - The encoded action + +### Size + +- **Minimum**: 4 bytes (zero actions) +- **Maximum**: computed as follows: + - Per-action overhead: `action_len` prefix (4) + `MAX_SINGLE_ACTION_BYTES` (16,424) = 16,428 bytes + - Total: 4 + `MAX_ACTIONS_PER_OUTPUT` × 16,428 = 4 + 64 × 16,428 = **1,051,396 bytes** + +### Validation Rules + +1. `action_count` MUST NOT exceed `MAX_ACTIONS_PER_OUTPUT` (64) +2. Each `action_len` MUST NOT exceed `MAX_SINGLE_ACTION_BYTES` (16,424) +3. Each `action_len` MUST exactly equal the number of bytes consumed by the following ActionV1 encoding +4. Exactly `action_count` actions MUST be present; fewer bytes implies `UnexpectedEndOfInput`, more bytes implies `InvalidLength` +5. Total bytes MUST equal the sum of all prefixes and action encodings (no trailing bytes) + +--- + +## Canonical Ordering + +Actions MUST be sorted into canonical order before encoding. This ensures deterministic `action_commitment` regardless of the order agents produce actions. + +### Ordering Rules + +Actions are sorted using lexicographic comparison in this priority: + +1. `action_type` (ascending, unsigned integer comparison) +2. `target` (lexicographic byte comparison, 32 bytes) +3. `payload` (lexicographic byte comparison of raw payload bytes) + +**Important**: The `payload_len` field is **not** part of the sort key; only the raw payload bytes are compared. Actions with identical `action_type`, `target`, and `payload` bytes are considered equal regardless of encoding. + +### Example + +Given actions: +- A: `{type: 2, target: 0x11..., payload: [1]}` +- B: `{type: 1, target: 0x22..., payload: [2]}` +- C: `{type: 1, target: 0x11..., payload: [3]}` + +Canonical order: **C, B, A** + +Reasoning: +1. C and B have `action_type=1`, A has `action_type=2` → A comes last +2. Between C and B: C has `target=0x11...`, B has `target=0x22...` → C comes first (0x11 < 0x22) + +--- + +## Commitment Computation + +### Input Commitment + +``` +input_commitment = SHA-256(encoded_KernelInputV1) +``` + +The commitment is computed over the **complete canonical encoding** of KernelInputV1, including: +- All fixed header fields (148 bytes) +- The `opaque_agent_inputs_len` length prefix (4 bytes) +- The `opaque_agent_inputs` data bytes + +### Action Commitment + +``` +action_commitment = SHA-256(encoded_AgentOutput) +``` + +The commitment is computed over the **complete canonical encoding** of AgentOutput, including: +- The `action_count` field (4 bytes) +- All `action_len` prefixes and ActionV1 encodings +- Actions MUST be in canonical order (see Canonical Ordering) + +--- + +## Error Handling + +### Codec Errors + +| Error | Condition | +|-------|-----------| +| `UnexpectedEndOfInput` | Insufficient bytes to decode a field | +| `InvalidLength` | Trailing bytes after complete structure, or length mismatch | +| `InvalidVersion` | Protocol or kernel version does not match expected constant | +| `InputTooLarge` | `opaque_agent_inputs_len` > 64,000 | +| `ActionPayloadTooLarge` | `payload_len` > 16,384 | +| `TooManyActions` | `action_count` > 64 | +| `ActionTooLarge` | Individual action encoding > 16,424 bytes | +| `InvalidExecutionStatus` | Status byte is not 0x01 | +| `ArithmeticOverflow` | Length calculation (e.g., offset + field_len) would overflow | + +### Strict Decoding + +Decoders MUST reject: +- Inputs with trailing bytes beyond the expected structure size +- Unknown or unsupported version numbers (protocol_version or kernel_version) +- Out-of-range size values (exceeding defined maximums) +- Invalid enumeration values (execution_status ≠ 0x01) +- Any computation where `offset + field_len` would overflow `usize` + +--- + +## Constants + +| Constant | Value | Description | +|----------|-------|-------------| +| `PROTOCOL_VERSION` | 1 | Current protocol version | +| `KERNEL_VERSION` | 1 | Current kernel version | +| `MAX_AGENT_INPUT_BYTES` | 64,000 | Maximum agent input size (bytes) | +| `MAX_ACTION_PAYLOAD_BYTES` | 16,384 | Maximum action payload size (bytes) | +| `MAX_ACTIONS_PER_OUTPUT` | 64 | Maximum actions per output | +| `MAX_SINGLE_ACTION_BYTES` | 16,424 | Maximum encoded ActionV1 size (40 + 16,384) | +| `JOURNAL_SIZE` | 209 | Fixed KernelJournalV1 size (bytes) | + +**Note**: `MAX_AGENT_INPUT_BYTES` is 64,000 bytes, not 64 KiB (65,536). This is an intentional limit. + +--- + +## Test Vectors + +See `tests/vectors/` for golden test vectors including: +- `kernel_input_v1.json` - KernelInputV1 encoding vectors +- `kernel_journal_v1.json` - KernelJournalV1 encoding vectors + +Each vector file contains: +- Positive vectors with fields, encoded hex, and commitment hex +- Negative vectors with invalid encodings and expected errors diff --git a/tests/vectors/kernel_input_v1.json b/tests/vectors/kernel_input_v1.json new file mode 100644 index 0000000..37af3d2 --- /dev/null +++ b/tests/vectors/kernel_input_v1.json @@ -0,0 +1,64 @@ +{ + "description": "Test vectors for KernelInputV1 canonical encoding", + "encoding_version": 1, + "vectors": [ + { + "name": "minimal_zeros", + "description": "Minimal valid input with all zeros and empty agent input", + "fields": { + "protocol_version": 1, + "kernel_version": 1, + "agent_id": "0000000000000000000000000000000000000000000000000000000000000000", + "agent_code_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "constraint_set_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "input_root": "0000000000000000000000000000000000000000000000000000000000000000", + "execution_nonce": 0, + "opaque_agent_inputs": "" + }, + "encoded_hex": "01000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "commitment_hex": "f0b4a449964d5ff3e473605e3ed1af1223f60135392d8add3244d2926ab9ab3f" + }, + { + "name": "standard_case", + "description": "Standard case with typical field values", + "fields": { + "protocol_version": 1, + "kernel_version": 1, + "agent_id": "4242424242424242424242424242424242424242424242424242424242424242", + "agent_code_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "constraint_set_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "input_root": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "execution_nonce": 12345, + "opaque_agent_inputs": "0102030405" + }, + "encoded_hex": "01000000010000004242424242424242424242424242424242424242424242424242424242424242aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc3930000000000000050000000102030405", + "commitment_hex": "6e4a2cce578937164ab4c0016a678b8e9d24a729c7c418b793b447fd299ff6a4" + } + ], + "negative_vectors": [ + { + "name": "truncated", + "description": "Input truncated to only 2 bytes", + "encoded_hex": "0102", + "expected_error": "UnexpectedEndOfInput" + }, + { + "name": "trailing_bytes", + "description": "Valid encoding with extra trailing byte 0xff", + "encoded_hex": "01000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff", + "expected_error": "InvalidLength" + }, + { + "name": "wrong_protocol_version", + "description": "Protocol version set to 999 instead of 1", + "encoded_hex": "e7030000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected_error": "InvalidVersion" + }, + { + "name": "empty_input", + "description": "Zero bytes - cannot decode anything", + "encoded_hex": "", + "expected_error": "UnexpectedEndOfInput" + } + ] +} diff --git a/tests/vectors/kernel_journal_v1.json b/tests/vectors/kernel_journal_v1.json new file mode 100644 index 0000000..f4e30c1 --- /dev/null +++ b/tests/vectors/kernel_journal_v1.json @@ -0,0 +1,77 @@ +{ + "description": "Test vectors for KernelJournalV1 canonical encoding", + "encoding_version": 1, + "fixed_size_bytes": 209, + "vectors": [ + { + "name": "minimal_zeros", + "description": "Minimal valid journal with all zeros", + "fields": { + "protocol_version": 1, + "kernel_version": 1, + "agent_id": "0000000000000000000000000000000000000000000000000000000000000000", + "agent_code_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "constraint_set_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "input_root": "0000000000000000000000000000000000000000000000000000000000000000", + "execution_nonce": 0, + "input_commitment": "0000000000000000000000000000000000000000000000000000000000000000", + "action_commitment": "0000000000000000000000000000000000000000000000000000000000000000", + "execution_status": "Success" + }, + "encoded_hex": "0100000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "standard_case", + "description": "Standard case with typical field values", + "fields": { + "protocol_version": 1, + "kernel_version": 1, + "agent_id": "4242424242424242424242424242424242424242424242424242424242424242", + "agent_code_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "constraint_set_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "input_root": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "execution_nonce": 12345, + "input_commitment": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "action_commitment": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "execution_status": "Success" + }, + "encoded_hex": "01000000010000004242424242424242424242424242424242424242424242424242424242424242aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc3930000000000000ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee01" + } + ], + "negative_vectors": [ + { + "name": "truncated", + "description": "Journal truncated to only 100 bytes", + "encoded_hex": "01000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected_error": "InvalidLength" + }, + { + "name": "trailing_bytes", + "description": "Valid 209-byte encoding with extra trailing byte", + "encoded_hex": "0100000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ff", + "expected_error": "InvalidLength" + }, + { + "name": "invalid_execution_status_zero", + "description": "Execution status byte is 0x00 (reserved/invalid)", + "encoded_hex": "0100000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "expected_error": "InvalidExecutionStatus" + }, + { + "name": "invalid_execution_status_ff", + "description": "Execution status byte is 0xFF (invalid)", + "encoded_hex": "01000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff", + "expected_error": "InvalidExecutionStatus" + }, + { + "name": "wrong_protocol_version", + "description": "Protocol version set to 999 instead of 1", + "encoded_hex": "e703000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + "expected_error": "InvalidVersion" + } + ], + "execution_status_encoding": { + "Success": "0x01", + "reserved_invalid": "0x00" + } +}