diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index a04e920db..9b3d6cd53 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -412,7 +412,7 @@ fn cmd_execute( // below (the flamegraph path drives execution inside the executor and does // not expose per-log data). `None` means "not counted", so the accel lines // are omitted rather than printed as misleading zeros. - let mut accel_counts: Option<(u64, u64)> = None; + let mut accel_counts: Option<(u64, u64, u64)> = None; let cycle_count = if let Some(ref output_path) = flamegraph.path { // Shared execute+flamegraph path (executor::flamegraph) instead of @@ -479,6 +479,7 @@ fn cmd_execute( let mut cycle_count: u64 = 0; let mut keccak_calls: u64 = 0; + let mut blake3_calls: u64 = 0; let mut ecsm_calls: u64 = 0; // Reused per chunk: `(current_pc, a7)` for logs whose a7 matches an // accelerator syscall number. This is a cheap superset — a non-ECALL @@ -511,6 +512,7 @@ fn cmd_execute( for (pc, a7) in accel_candidates.drain(..) { match accelerator_of(executor.instructions.get(pc), a7) { Some(Accelerator::Keccak) => keccak_calls += 1, + Some(Accelerator::Blake3) => blake3_calls += 1, Some(Accelerator::Ecsm) => ecsm_calls += 1, None => {} } @@ -526,15 +528,16 @@ fn cmd_execute( } if cycles { - accel_counts = Some((keccak_calls, ecsm_calls)); + accel_counts = Some((keccak_calls, blake3_calls, ecsm_calls)); } cycle_count }; if cycles { println!("Cycles: {}", cycle_count); - if let Some((keccak_calls, ecsm_calls)) = accel_counts { + if let Some((keccak_calls, blake3_calls, ecsm_calls)) = accel_counts { println!("Keccak calls: {}", keccak_calls); + println!("Blake3 calls: {}", blake3_calls); println!("Ecsm calls: {}", ecsm_calls); } } diff --git a/executor/programs/asm/test_blake3.s b/executor/programs/asm/test_blake3.s new file mode 100644 index 000000000..d066f4645 --- /dev/null +++ b/executor/programs/asm/test_blake3.s @@ -0,0 +1,60 @@ + .attribute 5, "rv64i2p1_m2p0_zmmul1p0" + .globl main +main: + # 176 bytes on the stack for the BLAKE3 state region (22 x u64): + # h[4 dwords] | m[8] | t[1] | block_len,flags[1] | out[8]. + addi sp, sp, -176 + + # Deterministic non-zero seed over the 14 input dwords: dword[k] = k + 1. + # (t therefore = 13, block_len = 14, flags = 0 — arbitrary but fixed.) + mv t0, sp + li t1, 1 + li t2, 15 +.Linit_loop: + sd t1, 0(t0) + addi t0, t0, 8 + addi t1, t1, 1 + bne t1, t2, .Linit_loop + + # First compression. + # a0 = pointer to the 176-byte region (8-aligned) + # a7 = syscall number (u64::MAX - 2 = -3) + mv a0, sp + li a7, -3 + ecall + + # Chain: copy out (8 dwords at sp+112) over m (8 dwords at sp+32), so the + # second call consumes the first call's output AND its out-region write has + # non-zero previous content. + li t1, 0 +.Lcopy_loop: + slli t2, t1, 3 + addi t3, sp, 112 + add t3, t3, t2 + ld t4, 0(t3) + addi t3, sp, 32 + add t3, t3, t2 + sd t4, 0(t3) + addi t1, t1, 1 + li t2, 8 + bne t1, t2, .Lcopy_loop + + # Second compression. + mv a0, sp + li a7, -3 + ecall + + # Commit the final 64-byte output. + li a0, 1 + addi a1, sp, 112 + li a2, 64 + li a7, 64 + ecall + + # Restore stack and halt. + addi sp, sp, 176 + li a0, 0 + li a7, 93 + ecall +.Lfunc_end0: + .size main, .Lfunc_end0-main diff --git a/executor/src/tests/blake3_tests.rs b/executor/src/tests/blake3_tests.rs new file mode 100644 index 000000000..98bd9af5d --- /dev/null +++ b/executor/src/tests/blake3_tests.rs @@ -0,0 +1,319 @@ +//! Tests for the BLAKE3 6-round compression and its accelerator syscall. +//! +//! Ground truth is the validated oracle (`thoughts/blake3/blake3-oracle/`): +//! the pinned canonical 6-round vectors below were emitted by its harness and +//! checked against the official `blake3` crate at the official test-vector +//! parameters. The `t` values exercise the full 64-bit counter range, pinning +//! the `t_lo → v[12]` / `t_hi → v[13]` split order. + +use crate::vm::instruction::decoding::Instruction; +use crate::vm::instruction::execution::{ + BLAKE3_SYSCALL_NUMBER, ExecutionError, blake3_compress_6round, +}; +use crate::vm::memory::Memory; +use crate::vm::registers::Registers; + +/// One pinned 6-round vector from the validated oracle. +struct Blake3Vector { + h: [u32; 8], + m: [u32; 16], + t: u64, + block_len: u32, + flags: u32, + out: [u32; 16], +} + +/// The 10 canonical 6-round vectors, generated from +/// `thoughts/blake3/blake3-oracle/canonical_6round_vectors.json` (which the +/// oracle harness regenerates and which was validated against the official +/// `blake3` crate). Do not edit by hand. +const CANONICAL_6ROUND_VECTORS: &[Blake3Vector] = &[ + Blake3Vector { + h: [ + 0xd82c07cd, 0x6baa9455, 0x82e2e662, 0x7a024204, 0xe87a1613, 0x81332876, 0x48268673, + 0xc17c6279, + ], + m: [ + 0xe6f4590b, 0x4f65d4d9, 0xbad640fb, 0xaf19922a, 0x19c78df4, 0x6f25e2a2, 0xe9bb17bc, + 0x7a1d5006, 0x42af9fc3, 0x03983ca8, 0xde1b372a, 0xded733e8, 0x9148624f, 0xf7b0b7d2, + 0x72ae2244, 0xeece328b, + ], + t: 0xb4e1357d4a84eb03, + block_len: 42, + flags: 52, + out: [ + 0xced9d1ff, 0xc248eeab, 0xbd109b7f, 0x911b48f6, 0x923d62c0, 0xd804903f, 0x5974223e, + 0xaa4f0c80, 0xad61007f, 0xb50b8ddb, 0xe7372be1, 0x33d3d6c3, 0x42aa284b, 0xc5a25f28, + 0x79ac8370, 0xb75f3915, + ], + }, + Blake3Vector { + h: [ + 0xc386bbc4, 0x414c343c, 0x7311d8a3, 0xa6cecc1b, 0xc9e9c616, 0x18072e8c, 0xd5f4b3b2, + 0x7204e52d, + ], + m: [ + 0xf1fd42a2, 0xe6c3f339, 0x07d4bedc, 0x8a9a021e, 0x3bab6c39, 0x05805975, 0xa46d6753, + 0xdc2574bd, 0xab99254a, 0x4da98f1d, 0xe1ea24c4, 0x815a47c5, 0x08d6af57, 0xcc22af58, + 0x2c4a3698, 0x5fec898f, + ], + t: 0xc74803e31ba16215, + block_len: 50, + flags: 94, + out: [ + 0xf2a972e9, 0x81fdb8ec, 0x40c50ebc, 0x4ba1caf9, 0x9ee9e930, 0x6b1a16b2, 0xe9156f47, + 0xa89fb436, 0xa2f616b3, 0x12874c12, 0x30768035, 0xe01a17d9, 0xbee5c17c, 0xd61c0be0, + 0x3041ff46, 0xdfb91125, + ], + }, + Blake3Vector { + h: [ + 0x0e7a269f, 0x15ba2bdd, 0xd5e34124, 0x4ee207f8, 0x9b1f282e, 0x9b575bd1, 0xf30b94fa, + 0x0706a045, + ], + m: [ + 0x6148a86f, 0x8697bbd0, 0x8f7d9b78, 0x3c729578, 0x061b9030, 0x533c9135, 0x829e07b0, + 0xe4c11ab2, 0xcbf87544, 0xc34c769f, 0x5a91c89b, 0xf63f23d0, 0xc1066932, 0x87c56473, + 0x7d718d73, 0xecc1cb63, + ], + t: 0x7604e4b4e73695c3, + block_len: 58, + flags: 124, + out: [ + 0x5aa6b114, 0xc9d6740c, 0x8738caf4, 0xac5f4b72, 0x9fc6b9de, 0x3f2efb8f, 0x8cb7a912, + 0xf497a285, 0x3d062266, 0x7f22380c, 0xafd468fa, 0x122cba80, 0x446b156d, 0xb239d8c2, + 0xc3eab2cf, 0x775f2f92, + ], + }, + Blake3Vector { + h: [ + 0x8b529b4a, 0x9a9a80fd, 0xd6645fa9, 0x3bfd1d33, 0x79f248b0, 0x268ecc45, 0xa2863a7f, + 0x85ef3430, + ], + m: [ + 0xbdc2ae99, 0x10645d51, 0x97524d6a, 0xdd933160, 0xe0f9e038, 0xebcd1f5e, 0xef829c88, + 0xe0fd67dd, 0x18f2c41c, 0x22cedafb, 0x378c74dc, 0x4d100d8f, 0x95c76ab4, 0x95918694, + 0xe779c470, 0xedcf6109, + ], + t: 0x92d3043afcf249f3, + block_len: 36, + flags: 31, + out: [ + 0xeed92fab, 0x138d9358, 0x915bfe3c, 0x13718b01, 0xb506e277, 0xbe4007cd, 0x35847e06, + 0xce1c6896, 0x52fa01b5, 0x4aa26af8, 0xb1078a61, 0x2c517aed, 0xa08867a0, 0xea6ecfea, + 0x6d33d3b0, 0xdc293166, + ], + }, + Blake3Vector { + h: [ + 0x3c6da5d7, 0x656412a9, 0x27ac435a, 0x11072231, 0xeaff1a09, 0xc3e1b258, 0x8963dc6e, + 0x1b2ed40e, + ], + m: [ + 0xed6f0b09, 0xce80c4b0, 0xccea2645, 0x3184ff27, 0x4f5253a0, 0xe14b0190, 0x9b191bf4, + 0xabf4a07c, 0x81862fc9, 0x2d83a823, 0x793d0e45, 0x4cdce7a6, 0xe8abb93f, 0xe1df8af9, + 0x8224b122, 0x69f85e31, + ], + t: 0x49c7b59b995253fd, + block_len: 57, + flags: 41, + out: [ + 0xca00bda3, 0x84239a3a, 0xe7c88e6d, 0x33a8a3d6, 0x09dcd1ce, 0xa1b10212, 0xf48e1156, + 0x8f039915, 0x8a055eaa, 0xff5b11d5, 0xb725085b, 0x2e1ab267, 0x6ae7323d, 0xb2ff6fa8, + 0x7102c8a1, 0x7561eb37, + ], + }, + Blake3Vector { + h: [ + 0x9f767c45, 0xbde5c099, 0xf17fd374, 0xa6233255, 0xe6a16a3b, 0x1cfb10f6, 0x3f1f65a8, + 0x8b33e968, + ], + m: [ + 0x92edcf45, 0x377b9aa2, 0x478c281d, 0xc4069545, 0xcc11d357, 0x9e115e4b, 0x206f5c66, + 0xdf1461aa, 0xfb7ff337, 0xdf561d80, 0x4a0fe75d, 0xf6236bf2, 0x346c6e2b, 0xb0cde917, + 0xe4cc4132, 0x4c7d6df0, + ], + t: 0x6a3753915c76f18a, + block_len: 18, + flags: 67, + out: [ + 0x14a9f66f, 0x101bdfe8, 0x9b0a50dd, 0xee4bb45b, 0x7a914502, 0x77b3486b, 0x59bfc114, + 0xa1ad2afd, 0xc194dde6, 0x894ec54d, 0xad36c805, 0x9018f3f5, 0x165af5d8, 0x3e85b598, + 0x78e76653, 0xbb7a485d, + ], + }, + Blake3Vector { + h: [ + 0xd26b9496, 0x42f9a039, 0x001d9a88, 0x5f877031, 0xc527e279, 0x45cf8aa4, 0xcd4a5557, + 0xae9af169, + ], + m: [ + 0xaf895f5b, 0xd822e2f9, 0x17d7ab26, 0xccdf540b, 0xce06294d, 0x4a8b0188, 0xf38d2e64, + 0x5c41d5c5, 0xe8d5b9e3, 0x5c832a51, 0x9a0c1b76, 0x4de8344e, 0x96d2f9e0, 0x8677a5f2, + 0xa9a967c1, 0x323bbeaf, + ], + t: 0x390567c27bd6aa42, + block_len: 26, + flags: 3, + out: [ + 0x32a6ff70, 0xc30560bc, 0xd1c777c8, 0xf1871821, 0x7207ab54, 0x9f5b83c7, 0xb6561c5d, + 0x991e738f, 0xb38b62b9, 0x0ef6d156, 0x994becb1, 0x09a85d0e, 0x32221741, 0xada3cc5f, + 0x5b654ed6, 0x2a7a62b2, + ], + }, + Blake3Vector { + h: [ + 0x269e0d37, 0xa6a3a450, 0x892f902b, 0x81e74ef5, 0x099950d8, 0x6f03675a, 0x11e20b8f, + 0x6cad4a26, + ], + m: [ + 0xf29d0da9, 0x658cda14, 0xf9ebdacc, 0xdbc496cb, 0x4a23d596, 0x2e44158b, 0xa38fd547, + 0x5f557203, 0x34b9b5df, 0x506bf2ef, 0x7403e430, 0x4cbd87ad, 0xcb5c7427, 0x3e7d1bfb, + 0x930d6eaf, 0x86734721, + ], + t: 0x12bd4acefaecbd38, + block_len: 53, + flags: 42, + out: [ + 0xa632ad45, 0x12ce41f4, 0xd21b2cbd, 0x76795c62, 0x6bec36c1, 0xdafafcde, 0x53ca87b7, + 0x92e8465b, 0x7b424f5d, 0xe1e6ad7f, 0x753ba387, 0xccc50824, 0x69aedf6d, 0xbbbbf253, + 0x78d04883, 0xf3f33689, + ], + }, + Blake3Vector { + h: [ + 0x3a096533, 0xf658f7a7, 0x205738d1, 0xb46ee1da, 0x15ceb3a1, 0x359b1548, 0xa4517d6c, + 0x7589ca4a, + ], + m: [ + 0x74007cb4, 0xd49d0ac1, 0x16edc5d4, 0x685ca8af, 0x4223aa56, 0x10269470, 0x60908405, + 0xa92d04a3, 0x56a3e957, 0xb0f91306, 0xe6c08269, 0xf2306d4a, 0x31a06a7c, 0x9436d6f6, + 0xe18692e2, 0xe0c99f3e, + ], + t: 0x329911da9fbd8735, + block_len: 19, + flags: 91, + out: [ + 0x913b2ae1, 0xc7f73082, 0x45e1c023, 0x6f1f3f82, 0x20aee6f5, 0xdaf21d94, 0xf2c1e4af, + 0xd4f7d4ac, 0x44a45f87, 0xf4c40ce5, 0x613e9b94, 0x08ce53de, 0x4ff07aa4, 0x456bf2e2, + 0x2066ea7f, 0x3c5a654b, + ], + }, + Blake3Vector { + h: [ + 0x5f915ef0, 0x237751aa, 0x01a5ba50, 0x80b65386, 0x14b044d7, 0x61076dc3, 0xb99de255, + 0x283b73a6, + ], + m: [ + 0x3cee5e2c, 0x1c670ea9, 0x972651da, 0x4a8aa593, 0xac9abb0c, 0x35bb5c11, 0x47fbb3b4, + 0xcf3c17e5, 0xe2eb17c8, 0xe11e99fb, 0x7de0d208, 0x0602fe0c, 0x98cae043, 0x9425b3e2, + 0x33fb4b4f, 0x15607df9, + ], + t: 0xeaeb999b8a2e547e, + block_len: 64, + flags: 21, + out: [ + 0xf5ee9114, 0x856cabb8, 0x29be2cf1, 0x603be91c, 0x94a7dd0e, 0x28fc3e27, 0xb64e2cc8, + 0x2d2c67ff, 0x69fac1ba, 0x0c949090, 0xd68de435, 0xce91a527, 0xe80c1815, 0x6d44efe6, + 0x87c7b175, 0xd18a8b94, + ], + }, +]; + +#[test] +fn test_blake3_6round_canonical_vectors() { + for (i, v) in CANONICAL_6ROUND_VECTORS.iter().enumerate() { + let out = blake3_compress_6round(&v.h, &v.m, v.t, v.block_len, v.flags); + assert_eq!(out, v.out, "canonical 6-round vector {i} mismatch"); + } +} + +#[test] +fn test_blake3_syscall_matches_vectors() { + for (i, v) in CANONICAL_6ROUND_VECTORS.iter().enumerate() { + let mut pc = 0; + let mut registers = Registers::default(); + let mut memory = Memory::default(); + let addr = 0x1000u64; + + // Lay out the 176-byte state region: h | m | t | (block_len, flags) | out. + let mut words = [0u32; 28]; + words[0..8].copy_from_slice(&v.h); + words[8..24].copy_from_slice(&v.m); + words[24] = v.t as u32; + words[25] = (v.t >> 32) as u32; + words[26] = v.block_len; + words[27] = v.flags; + for k in 0..14 { + let dw = (words[2 * k] as u64) | ((words[2 * k + 1] as u64) << 32); + memory.store_doubleword(addr + (k as u64) * 8, dw).unwrap(); + } + // Pre-fill the out region so the test catches a partial write. + for k in 14..22 { + memory + .store_doubleword(addr + (k as u64) * 8, 0xDEAD_BEEF_DEAD_BEEFu64) + .unwrap(); + } + + registers.write(17, BLAKE3_SYSCALL_NUMBER).unwrap(); + registers.write(10, addr).unwrap(); + Instruction::EcallEbreak + .run(&mut pc, &mut registers, &mut memory) + .unwrap(); + + let mut got = [0u32; 16]; + for k in 0..8 { + let dw = memory + .load_doubleword(addr + ((14 + k) as u64) * 8) + .unwrap(); + got[2 * k] = dw as u32; + got[2 * k + 1] = (dw >> 32) as u32; + } + assert_eq!(got, v.out, "syscall output mismatch on vector {i}"); + + // The 112 input bytes must be untouched. + for k in 0..14 { + let dw = memory.load_doubleword(addr + (k as u64) * 8).unwrap(); + let expected = (words[2 * k] as u64) | ((words[2 * k + 1] as u64) << 32); + assert_eq!(dw, expected, "input dword {k} clobbered on vector {i}"); + } + } +} + +#[test] +fn test_blake3_syscall_rejects_unaligned_state_addr() { + let mut pc = 0; + let mut registers = Registers::default(); + let mut memory = Memory::default(); + + registers.write(17, BLAKE3_SYSCALL_NUMBER).unwrap(); + registers.write(10, 0x1004).unwrap(); + + let err = Instruction::EcallEbreak + .run(&mut pc, &mut registers, &mut memory) + .unwrap_err(); + assert!(matches!( + err, + ExecutionError::UnalignedBlake3StateAddress(0x1004) + )); +} + +#[test] +fn test_blake3_syscall_rejects_overflowing_state_range() { + let mut pc = 0; + let mut registers = Registers::default(); + let mut memory = Memory::default(); + + registers.write(17, BLAKE3_SYSCALL_NUMBER).unwrap(); + // 22 dwords = 176 bytes; addr + 175 must not overflow. u64::MAX - 167 is + // 8-aligned and the last byte lands at u64::MAX + 8 → overflow. + registers.write(10, u64::MAX - 167).unwrap(); + + let err = Instruction::EcallEbreak + .run(&mut pc, &mut registers, &mut memory) + .unwrap_err(); + assert!(matches!( + err, + ExecutionError::Blake3StateAddressOverflow(addr) if addr == u64::MAX - 167 + )); +} diff --git a/executor/src/tests/mod.rs b/executor/src/tests/mod.rs index 456607433..90bb1331c 100644 --- a/executor/src/tests/mod.rs +++ b/executor/src/tests/mod.rs @@ -1,3 +1,4 @@ +pub mod blake3_tests; pub mod ecsm_tests; pub mod flamegraph_tests; pub mod keccak_tests; diff --git a/executor/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index c92c0ab88..7e934c585 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -12,6 +12,8 @@ pub enum SyscallNumbers { KeccakPermute = 0, Print = 1, Panic = 2, + // Placeholder discriminant. The actual syscall value is BLAKE3_SYSCALL_NUMBER. + Blake3Compress = 3, Commit = 64, Halt = 93, // Placeholder discriminant. The actual syscall value is ECSM_SYSCALL_NUMBER. @@ -24,6 +26,30 @@ pub enum SyscallNumbers { pub const KECCAK_SYSCALL_NUMBER: u64 = u64::MAX - 1; const KECCAK_STATE_BYTES: u64 = 25 * 8; +/// Syscall number for the BLAKE3 6-round compression accelerator +/// (u64::MAX - 2 = 0xFFFF_FFFF_FFFF_FFFD). +/// +/// This is the **6-round internal variant** of the BLAKE3 compression function +/// (`thoughts/blake3/blake3-chip/DESIGN.md`), intended for in-house Merkle / +/// Fiat–Shamir use — it is NOT standard 7-round BLAKE3 and its security rests +/// on the named 6-round assumption recorded in the design. +/// +/// ABI: `x10` = 8-byte-aligned pointer to a 176-byte state region laid out as +/// consecutive little-endian dwords at `addr + 8k`: +/// +/// | dword k | contents | +/// |---------|--------------------------------------------| +/// | 0..=3 | `h[0..8]` chaining value (2 u32 words/dword) | +/// | 4..=11 | `m[0..16]` message block | +/// | 12 | `t` counter (`t_lo = low u32 → v[12]`, `t_hi = high u32 → v[13]`) | +/// | 13 | `block_len` (low u32) \| `flags` (high u32) | +/// | 14..=21 | `out[0..16]` — written by the syscall | +pub const BLAKE3_SYSCALL_NUMBER: u64 = u64::MAX - 2; +/// Bytes of the BLAKE3 state region: 112 input + 64 output. +const BLAKE3_STATE_BYTES: u64 = 22 * 8; +/// Dword offset of `out[0..16]` inside the BLAKE3 state region. +const BLAKE3_OUT_DWORDS: u64 = 14; + /// Syscall number for the ECSM (elliptic-curve scalar multiply) accelerator. /// /// The spec uses ECALL number `-11`; interpreted as an unsigned 64-bit value that is @@ -44,6 +70,7 @@ impl TryFrom for SyscallNumbers { 64 => Ok(SyscallNumbers::Commit), 93 => Ok(SyscallNumbers::Halt), v if v == KECCAK_SYSCALL_NUMBER => Ok(SyscallNumbers::KeccakPermute), + v if v == BLAKE3_SYSCALL_NUMBER => Ok(SyscallNumbers::Blake3Compress), v if v == ECSM_SYSCALL_NUMBER => Ok(SyscallNumbers::Ecsm), _ => Err(()), } @@ -54,6 +81,7 @@ impl TryFrom for SyscallNumbers { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Accelerator { Keccak, + Blake3, Ecsm, } @@ -64,6 +92,7 @@ impl SyscallNumbers { pub fn accelerator(self) -> Option { match self { SyscallNumbers::KeccakPermute => Some(Accelerator::Keccak), + SyscallNumbers::Blake3Compress => Some(Accelerator::Blake3), SyscallNumbers::Ecsm => Some(Accelerator::Ecsm), SyscallNumbers::Print | SyscallNumbers::Panic @@ -421,6 +450,41 @@ impl Instruction { } src2_val = state_addr; } + SyscallNumbers::Blake3Compress => { + // BLAKE3 6-round compression on a 176-byte region at the + // address in x10 (layout: see BLAKE3_SYSCALL_NUMBER docs). + let state_addr = registers.read(10)?; + if !state_addr.is_multiple_of(8) { + return Err(ExecutionError::UnalignedBlake3StateAddress(state_addr)); + } + state_addr + .checked_add(BLAKE3_STATE_BYTES - 1) + .ok_or(ExecutionError::Blake3StateAddressOverflow(state_addr))?; + + // Input: 14 dwords = h[8] | m[16] | t | (block_len, flags), + // each dword two little-endian u32 words. + let mut words = [0u32; 28]; + for k in 0..14 { + let dw = memory.load_doubleword(state_addr + (k as u64) * 8)?; + words[2 * k] = dw as u32; + words[2 * k + 1] = (dw >> 32) as u32; + } + let h: [u32; 8] = words[0..8].try_into().unwrap(); + let m: [u32; 16] = words[8..24].try_into().unwrap(); + let t = (words[24] as u64) | ((words[25] as u64) << 32); + let block_len = words[26]; + let flags = words[27]; + + let out = blake3_compress_6round(&h, &m, t, block_len, flags); + for k in 0..8 { + let dw = (out[2 * k] as u64) | ((out[2 * k + 1] as u64) << 32); + memory.store_doubleword( + state_addr + (BLAKE3_OUT_DWORDS + k as u64) * 8, + dw, + )?; + } + src2_val = state_addr; + } SyscallNumbers::Ecsm => { // ECSM(-11): k×G on secp256k1. // x10 = addr to write xR, x11 = addr of xG, x12 = addr of k. @@ -630,6 +694,10 @@ pub enum ExecutionError { UnalignedKeccakStateAddress(u64), #[error("Keccak state address range overflows: {0:#018x}")] KeccakStateAddressOverflow(u64), + #[error("Unaligned BLAKE3 state address: {0:#018x}")] + UnalignedBlake3StateAddress(u64), + #[error("BLAKE3 state address range overflows: {0:#018x}")] + Blake3StateAddressOverflow(u64), #[error("ECSM address range overflows the lower 32-bit limb")] EcsmAddressOverflow, #[error("ECSM xG and k operand ranges overlap")] @@ -719,3 +787,106 @@ pub fn keccak_f1600(state: &mut [u64; 25]) { state[0] ^= rc; } } + +// ============================================================================= +// BLAKE3 6-round compression (internal variant) +// ============================================================================= +// +// A Rust port of the validated oracle `thoughts/blake3/blake3-oracle/blake3_ref.py` +// with `rounds = 6` fixed. This is the **6-round internal variant** — NOT +// standard BLAKE3 (7 rounds); its security rests on the named 6-round +// assumption recorded in `thoughts/blake3/blake3-chip/DESIGN.md`. Differentially +// tested against the oracle's canonical 6-round vectors (pinned in +// `thoughts/blake3/blake3-oracle/canonical_6round_vectors.json`, themselves +// validated against the official `blake3` crate). + +/// The BLAKE3 IV (identical to SHA-256's initial state). `IV[0..4]` seeds +/// `v[8..12]` of the compression working state. +pub const BLAKE3_IV: [u32; 8] = [ + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, +]; + +/// The BLAKE3 message-schedule permutation, applied between rounds +/// (`m'[i] = m[MSG_PERMUTATION[i]]`). +pub const BLAKE3_MSG_PERMUTATION: [usize; 16] = + [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8]; + +/// Rounds of the internal variant. 6, per the design; standard BLAKE3 is 7. +pub const BLAKE3_ROUNDS: usize = 6; + +/// The BLAKE3 quarter-round G (spec §2.1). +#[inline] +fn blake3_g(v: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize, mx: u32, my: u32) { + v[a] = v[a].wrapping_add(v[b]).wrapping_add(mx); + v[d] = (v[d] ^ v[a]).rotate_right(16); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(12); + v[a] = v[a].wrapping_add(v[b]).wrapping_add(my); + v[d] = (v[d] ^ v[a]).rotate_right(8); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(7); +} + +/// The BLAKE3 compression function `f` at 6 rounds (spec §2.2, oracle §2.4). +/// +/// State init: `v[0..8] = h`, `v[8..12] = IV[0..4]`, `v[12] = t as u32`, +/// `v[13] = (t >> 32) as u32`, `v[14] = block_len`, `v[15] = flags`. Six +/// rounds of 8 G-calls (4 columns then 4 diagonals), permuting the message +/// schedule between rounds (`r < rounds - 1`, i.e. 5 permutes — the trailing +/// permute is never consumed). Feed-forward: `out[i] = v[i] ^ v[i+8]`, +/// `out[i+8] = v[i+8] ^ h[i]`. The truncated chaining value is `out[0..8]`. +pub fn blake3_compress_6round( + h: &[u32; 8], + m: &[u32; 16], + t: u64, + block_len: u32, + flags: u32, +) -> [u32; 16] { + let mut v: [u32; 16] = [ + h[0], + h[1], + h[2], + h[3], + h[4], + h[5], + h[6], + h[7], + BLAKE3_IV[0], + BLAKE3_IV[1], + BLAKE3_IV[2], + BLAKE3_IV[3], + t as u32, + (t >> 32) as u32, + block_len, + flags, + ]; + + let mut m = *m; + for r in 0..BLAKE3_ROUNDS { + // Mix the columns. + blake3_g(&mut v, 0, 4, 8, 12, m[0], m[1]); + blake3_g(&mut v, 1, 5, 9, 13, m[2], m[3]); + blake3_g(&mut v, 2, 6, 10, 14, m[4], m[5]); + blake3_g(&mut v, 3, 7, 11, 15, m[6], m[7]); + // Mix the diagonals. + blake3_g(&mut v, 0, 5, 10, 15, m[8], m[9]); + blake3_g(&mut v, 1, 6, 11, 12, m[10], m[11]); + blake3_g(&mut v, 2, 7, 8, 13, m[12], m[13]); + blake3_g(&mut v, 3, 4, 9, 14, m[14], m[15]); + // Permute between rounds; the permute after the last round is never + // consumed (oracle: `r < rounds - 1`). + if r < BLAKE3_ROUNDS - 1 { + let prev = m; + for (i, &p) in BLAKE3_MSG_PERMUTATION.iter().enumerate() { + m[i] = prev[p]; + } + } + } + + let mut out = [0u32; 16]; + for i in 0..8 { + out[i] = v[i] ^ v[i + 8]; + out[i + 8] = v[i + 8] ^ h[i]; + } + out +} diff --git a/prover/src/lib.rs b/prover/src/lib.rs index a8e89f989..7f3731fde 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -51,10 +51,10 @@ use crate::tables::trace_builder::Traces; use crate::tables::trace_builder::count_table_lengths; use crate::tables::types::BusId; use crate::test_utils::{ - E, F, VmAir, create_bitwise_air, create_branch_air, create_bytewise_air, create_commit_air, - create_cpu_air, create_cpu32_air, create_decode_air, create_dvrm_air, create_ecdas_air, - create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, create_keccak_rc_air, - create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, + E, F, VmAir, create_bitwise_air, create_blake3_air, create_branch_air, create_bytewise_air, + create_commit_air, create_cpu_air, create_cpu32_air, create_decode_air, create_dvrm_air, + create_ecdas_air, create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, + create_keccak_rc_air, create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, create_memw_aligned_air, create_memw_register_air, create_mul_air, create_page_air, create_register_air, create_shift_air, create_store_air, }; @@ -82,8 +82,13 @@ pub struct RuntimePageRange { /// Number of tables that always contribute exactly one sub-proof, regardless /// of `TableCounts`: bitwise, decode, halt, commit, keccak, keccak_rnd, -/// keccak_rc, register, ecsm, ecdas. -pub const FIXED_TABLE_COUNT: usize = 10; +/// keccak_rc, blake3, register, ecsm, ecdas. +/// +/// ⚠ Every always-on table costs every proof a near-empty AIR even when the +/// workload never touches it (the EC-campaign lesson, PR #871). BLAKE3 adds +/// one (min 4 rows × ~3.2k cols); its real-workload cost must be ABBA-checked +/// before this merges. +pub const FIXED_TABLE_COUNT: usize = 11; /// Number of chunks for each split table. /// The verifier needs this to reconstruct matching AIRs. @@ -515,6 +520,7 @@ pub(crate) struct VmAirs { pub keccak: VmAir, pub keccak_rnd: VmAir, pub keccak_rc: VmAir, + pub blake3: VmAir, pub ecsm: VmAir, pub ecdas: VmAir, pub register: VmAir, @@ -540,6 +546,7 @@ impl VmAirs { (self.keccak.as_ref(), &mut traces.keccak, &()), (self.keccak_rnd.as_ref(), &mut traces.keccak_rnd, &()), (self.keccak_rc.as_ref(), &mut traces.keccak_rc, &()), + (self.blake3.as_ref(), &mut traces.blake3, &()), (self.ecsm.as_ref(), &mut traces.ecsm, &()), (self.ecdas.as_ref(), &mut traces.ecdas, &()), (self.register.as_ref(), &mut traces.register, &()), @@ -614,6 +621,7 @@ impl VmAirs { self.keccak.as_ref(), self.keccak_rnd.as_ref(), self.keccak_rc.as_ref(), + self.blake3.as_ref(), self.ecsm.as_ref(), self.ecdas.as_ref(), self.register.as_ref(), @@ -767,6 +775,7 @@ impl VmAirs { let commit: VmAir = Box::new(create_commit_air(proof_options)); let keccak: VmAir = Box::new(create_keccak_air(proof_options)); let keccak_rnd: VmAir = Box::new(create_keccak_rnd_air(proof_options)); + let blake3: VmAir = Box::new(create_blake3_air(proof_options)); let keccak_rc: VmAir = Box::new(create_keccak_rc_air(proof_options).with_preprocessed( tables::keccak_rc::preprocessed_commitment(proof_options), tables::keccak_rc::NUM_PRECOMPUTED_COLS, @@ -877,6 +886,7 @@ impl VmAirs { keccak, keccak_rnd, keccak_rc, + blake3, ecsm, ecdas, register, diff --git a/prover/src/tables/blake3.rs b/prover/src/tables/blake3.rs new file mode 100644 index 000000000..86b7f1f9f --- /dev/null +++ b/prover/src/tables/blake3.rs @@ -0,0 +1,1278 @@ +//! BLAKE3 6-round compression accelerator chip (syscall variant). +//! +//! One row per compression call, fully unrolled (Layout B of +//! `thoughts/blake3/blake3-chip/DESIGN.md`): all 6 rounds × 8 G-functions are +//! laid out in SSA form across the row, so the message schedule is a +//! compile-time permutation of the 16 committed message words and there is no +//! state/message handoff between rows. +//! +//! I/O follows the KECCAK core idiom (`keccak.rs`): an `Ecall` receiver binds +//! (timestamp, syscall#), a `Memw` register read binds the x10 state pointer, +//! and per-dword `Memw` ops read the 112 input bytes / write the 64 output +//! bytes. The 176-byte state region layout is documented on +//! [`executor::vm::instruction::execution::BLAKE3_SYSCALL_NUMBER`]. +//! +//! ## The single-dataflow rule +//! +//! The compression dataflow is written ONCE, in [`run_flow`], and interpreted +//! twice: [`WireFlow`] (columns — drives the constraints and bus senders) and +//! [`ValueFlow`] (u32 witness — drives trace filling and the BITWISE +//! multiplicity collection in `trace_builder.rs`). The two cannot diverge on +//! wiring, only on interpretation, which the e2e bus-balance gate checks. +//! +//! ## Soundness ledger (DESIGN.md §7, adapted to the syscall variant) +//! +//! 1. Every eval constraint is μ-gated; padding rows are all-zero (except the +//! keccak-style PTR pad) with μ=0. +//! 2. 3-op adds use TWO summed committed carry bits + the explicit sum +//! identity (a ternary carry would be degree 4 after gating). +//! 3. 2-op adds use the `emit_add_pair`-style expression carry (no committed +//! cell) with μ-gated booleanity; the output's bytes are range-checked by +//! the downstream XOR lookup that consumes them. +//! 4. Every add/shift output feeds a downstream `ByteAlu` XOR — that lookup is +//! its only byte range check. The last-round outputs are consumed by the +//! feed-forward XORs, closing the chain. +//! 5. The message words `m` are never XORed, so their 64 bytes get explicit +//! `AreBytes` sends. Same for the 64 `OLD_OUT` bytes (the previous memory +//! content of the out region, which appear only on the Memw bus) and the 8 +//! address bytes (aliasing — see keccak.rs's addr comment). +//! 6. rotr16/rotr8 are free byte relabels `[b2,b3,b0,b1]` / `[b1,b2,b3,b0]`. +//! 7. rotr12/rotr7 are inline μ-gated shift identities with `AreBytes` on all +//! four shift halfwords (`SLL_lo/SLLC_lo/SLL_hi/SLLC_hi`); soundness needs +//! the tight bound on the `SLL` pair (2^16 invertible mod p — the audited +//! Euclidean-division argument). +//! 8. The message schedule is `permute^r` wired from the ORIGINAL M columns. +//! 9. All identities stay < 2^35 ≪ p (non-overflow side conditions), given +//! byte-range operands and boolean carries. +//! 10. (Internal-bus binding — N/A here: the syscall variant has no `Blake3` +//! bus; a row's inputs and outputs are tied by being the same row.) +//! +//! ⚠ This chip implements the **6-round internal variant** — NOT standard +//! 7-round BLAKE3. Its collision resistance is a named assumption +//! (DESIGN.md "If this is picked up again"). + +use executor::vm::instruction::execution::{ + BLAKE3_IV, BLAKE3_MSG_PERMUTATION, BLAKE3_ROUNDS, BLAKE3_SYSCALL_NUMBER, +}; +use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; +use stark::trace::TraceTable; + +use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; + +use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; +use crate::constraints::templates::{AddOperand, INV_SHIFT_32}; + +/// G-instances per compression: 8 per round × 6 rounds. +pub const NUM_G: usize = BLAKE3_ROUNDS * 8; + +/// Dwords in the state region: 14 input (h|m|t|len_flags) + 8 output. +pub const STATE_DWORDS: usize = 22; +/// Input dwords (read-only). +pub const IN_DWORDS: usize = 14; + +/// The (a, b, c, d) state indices of the 8 G-calls of one round: +/// 4 column mixes then 4 diagonal mixes (BLAKE3 spec §2.1). +const G_INDICES: [(usize, usize, usize, usize); 8] = [ + (0, 4, 8, 12), + (1, 5, 9, 13), + (2, 6, 10, 14), + (3, 7, 11, 15), + (0, 5, 10, 15), + (1, 6, 11, 12), + (2, 7, 8, 13), + (3, 4, 9, 14), +]; + +/// Shift amounts of the two non-free rotations, as `rotl` inner shifts: +/// rotr12 = rotl20 = rotl16∘rotl4 (r=4); rotr7 = rotl25 = rotl16∘rotl9 (r=9). +const ROT_SHIFT_R: [u32; 2] = [4, 9]; + +// ========================================================================= +// Column indices +// ========================================================================= + +pub mod cols { + use super::NUM_G; + + pub const TIMESTAMP_0: usize = 0; + pub const TIMESTAMP_1: usize = 1; + + /// State address as 8 bytes (DWordBL). + pub const ADDR: usize = 2; + + /// Per-dword pointers [22][4] halfwords (DWordHL), ptr[k] = addr + 8k. + pub const PTR: usize = ADDR + 8; // 10 + + /// Input bytes: h[32] | m[64] | t_lo[4] | t_hi[4] | block_len[4] | flags[4]. + pub const IN: usize = PTR + super::STATE_DWORDS * 4; // 98 + + /// 48 G-blocks × 60 cells (56 bytes + 4 carry bits) — see `g` accessors. + pub const G: usize = IN + 112; // 210 + pub const G_SIZE: usize = 60; + + /// Feed-forward output bytes out[0..16] (64 bytes). + pub const OUT: usize = G + NUM_G * G_SIZE; // 3090 + + /// Previous memory content of the out region (64 bytes). Appears only in + /// the Memw write ops' `old` field; range-checked by AreBytes. + pub const OLD_OUT: usize = OUT + 64; // 3154 + + /// Multiplicity / gate flag. + pub const MU: usize = OLD_OUT + 64; // 3218 + + pub const NUM_COLUMNS: usize = MU + 1; // 3219 + + // ------------------------------------------------------------------------- + // Index helpers + // ------------------------------------------------------------------------- + + #[inline] + pub const fn addr(byte: usize) -> usize { + ADDR + byte + } + + /// ptr[k][hw] — halfword hw of the pointer to dword k. + #[inline] + pub const fn ptr(k: usize, hw: usize) -> usize { + PTR + k * 4 + hw + } + + /// Input word i (0..28: h[0..8], m[8..24], t_lo=24, t_hi=25, len=26, flags=27), + /// byte b. + #[inline] + pub const fn in_word(i: usize, b: usize) -> usize { + IN + i * 4 + b + } + + /// Base column of G-block g. + #[inline] + pub const fn g_base(g: usize) -> usize { + G + g * G_SIZE + } + + // Offsets inside one G block (56 byte cells + 4 carry bits = 60): + /// add3 #1 output word (4 bytes). + pub const G_A1: usize = 0; + /// add3 #1 carry bits c1, c2. + pub const G_A1_C: usize = 4; + /// X1 = vd ^ A1 (4 bytes). + pub const G_X1: usize = 6; + /// add2 #1 output word (4 bytes). + pub const G_C1: usize = 10; + /// X2 = vb ^ C1 (4 bytes). + pub const G_X2: usize = 14; + /// rotr12 block: SLL_lo(2) SLLC_lo(2) SLL_hi(2) SLLC_hi(2) Y(4). + pub const G_R1: usize = 18; + /// add3 #2 output word (4 bytes). + pub const G_A2: usize = 30; + /// add3 #2 carry bits. + pub const G_A2_C: usize = 34; + /// X3 = vd ^ A2 (4 bytes). + pub const G_X3: usize = 36; + /// add2 #2 output word (4 bytes). + pub const G_C2: usize = 40; + /// X4 = B1 ^ C2 (4 bytes). + pub const G_X4: usize = 44; + /// rotr7 block: same layout as G_R1. + pub const G_R2: usize = 48; + + /// Feed-forward output word i (0..16), byte b. + #[inline] + pub const fn out_word(i: usize, b: usize) -> usize { + OUT + i * 4 + b + } + + /// Previous-content byte b (0..64) of the out region. + #[inline] + pub const fn old_out(b: usize) -> usize { + OLD_OUT + b + } +} + +// ========================================================================= +// The single dataflow, interpreted twice +// ========================================================================= + +/// The BLAKE3 compression dataflow, abstracted over its word representation. +/// +/// [`run_flow`] is the only place the G-function wiring, message schedule and +/// feed-forward exist; implementors interpret the primitive ops either as +/// column wiring ([`WireFlow`]) or as u32 witness computation ([`ValueFlow`]). +pub(crate) trait Blake3Flow { + type Word: Copy; + + /// h[i] input word. + fn input_h(&mut self, i: usize) -> Self::Word; + /// v[12..16] init words: t_lo, t_hi, block_len, flags. + fn input_v12(&mut self, j: usize) -> Self::Word; + /// IV[i] constant (v[8..12]). + fn iv_const(&mut self, i: usize) -> Self::Word; + + /// 3-operand add `s = a + b + m[m_idx] mod 2^32` (half 0/1 = which add3 of G g). + fn add3( + &mut self, + g: usize, + half: usize, + a: Self::Word, + b: Self::Word, + m_idx: usize, + ) -> Self::Word; + /// 2-operand add `s = a + b mod 2^32`. + fn add2(&mut self, g: usize, half: usize, a: Self::Word, b: Self::Word) -> Self::Word; + /// XOR (slot 0..4 = X1..X4 of G g). Operand order is part of the wire format. + fn xor(&mut self, g: usize, slot: usize, a: Self::Word, b: Self::Word) -> Self::Word; + /// rotr16: free byte relabel [b2,b3,b0,b1]. + fn rotr16(&mut self, w: Self::Word) -> Self::Word; + /// rotr8: free byte relabel [b1,b2,b3,b0]. + fn rotr8(&mut self, w: Self::Word) -> Self::Word; + /// rotr12 (half=0) / rotr7 (half=1) via the inline shift identity. + fn rot_shift(&mut self, g: usize, half: usize, w: Self::Word) -> Self::Word; + /// Feed-forward XOR pair: out[i] = v[i] ^ v[i+8], out[i+8] = v[i+8] ^ h[i]. + fn feed_forward(&mut self, i: usize, vi: Self::Word, vi8: Self::Word, hi: Self::Word); +} + +/// Drive the full 6-round compression through `f`. The message schedule is +/// tracked as indices into the ORIGINAL m (permute^r composition), so both +/// interpretations reference original message words — never copies. +pub(crate) fn run_flow(f: &mut F) { + let h: [F::Word; 8] = core::array::from_fn(|i| f.input_h(i)); + let mut v: [F::Word; 16] = core::array::from_fn(|i| { + if i < 8 { + h[i] + } else if i < 12 { + f.iv_const(i - 8) + } else { + f.input_v12(i - 12) + } + }); + + // sched[i] = index into the original m of the word consumed at position i + // this round. permute: m'[i] = m[P[i]] ⇒ sched'[i] = sched[P[i]]. + let mut sched: [usize; 16] = core::array::from_fn(|i| i); + + for r in 0..BLAKE3_ROUNDS { + for (j, &(ia, ib, ic, id)) in G_INDICES.iter().enumerate() { + let g = r * 8 + j; + let (va, vb, vc, vd) = (v[ia], v[ib], v[ic], v[id]); + let mx = sched[2 * j]; + let my = sched[2 * j + 1]; + + let a1 = f.add3(g, 0, va, vb, mx); + let x1 = f.xor(g, 0, vd, a1); + let vd1 = f.rotr16(x1); + let c1 = f.add2(g, 0, vc, vd1); + let x2 = f.xor(g, 1, vb, c1); + let b1 = f.rot_shift(g, 0, x2); // rotr12 + let a2 = f.add3(g, 1, a1, b1, my); + let x3 = f.xor(g, 2, vd1, a2); + let vd2 = f.rotr8(x3); + let c2 = f.add2(g, 1, c1, vd2); + let x4 = f.xor(g, 3, b1, c2); + let b2 = f.rot_shift(g, 1, x4); // rotr7 + + v[ia] = a2; + v[ib] = b2; + v[ic] = c2; + v[id] = vd2; + } + if r < BLAKE3_ROUNDS - 1 { + let prev = sched; + for (i, &p) in BLAKE3_MSG_PERMUTATION.iter().enumerate() { + sched[i] = prev[p]; + } + } + } + + for i in 0..8 { + f.feed_forward(i, v[i], v[i + 8], h[i]); + } +} + +// ========================================================================= +// Wire interpretation (columns) +// ========================================================================= + +/// A 32-bit word as wiring: four byte columns (LSB first) or a constant. +/// Constants only ever appear as the IV `v[c]` operands of round-0 add2s. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum WordRef { + Cols([usize; 4]), + Const(u32), +} + +impl WordRef { + fn byte(self, b: usize) -> ByteRef { + match self { + WordRef::Cols(c) => ByteRef::Col(c[b]), + WordRef::Const(w) => ByteRef::Const(((w >> (8 * b)) & 0xFF) as u8), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ByteRef { + Col(usize), + Const(u8), +} + +/// One recorded 3-op add: operands (a, b, m columns), output columns, carries. +pub(crate) struct Add3Wire { + pub a: WordRef, + pub b: WordRef, + pub m: [usize; 4], + pub s: [usize; 4], + pub c1: usize, + pub c2: usize, +} + +/// One recorded 2-op add: operands, output columns (carry is an expression). +pub(crate) struct Add2Wire { + pub a: WordRef, + pub b: WordRef, + pub s: [usize; 4], +} + +/// One recorded XOR: per-byte operands and output columns. +pub(crate) struct XorWire { + pub a: WordRef, + pub b: WordRef, + pub out: [usize; 4], +} + +/// One recorded shift rotation: input word, the 8 shift-halfword byte columns +/// (SLL_lo, SLLC_lo, SLL_hi, SLLC_hi — 2 bytes each), output columns, r. +pub(crate) struct RotWire { + pub input: WordRef, + pub sll_lo: [usize; 2], + pub sllc_lo: [usize; 2], + pub sll_hi: [usize; 2], + pub sllc_hi: [usize; 2], + pub y: [usize; 4], + pub r: u32, +} + +/// The full wiring of one compression row: everything the constraints and the +/// bus senders need, recorded in canonical order by [`run_flow`]. +pub(crate) struct WireFlow { + pub add3s: Vec, + pub add2s: Vec, + pub xors: Vec, + pub rots: Vec, +} + +impl WireFlow { + pub(crate) fn build() -> Self { + let mut w = WireFlow { + add3s: Vec::with_capacity(NUM_G * 2), + add2s: Vec::with_capacity(NUM_G * 2), + xors: Vec::with_capacity(NUM_G * 4 + 16), + rots: Vec::with_capacity(NUM_G * 2), + }; + run_flow(&mut w); + w + } +} + +#[inline] +fn word_cols(start: usize) -> [usize; 4] { + [start, start + 1, start + 2, start + 3] +} + +impl Blake3Flow for WireFlow { + type Word = WordRef; + + fn input_h(&mut self, i: usize) -> WordRef { + WordRef::Cols(word_cols(cols::in_word(i, 0))) + } + fn input_v12(&mut self, j: usize) -> WordRef { + WordRef::Cols(word_cols(cols::in_word(24 + j, 0))) + } + fn iv_const(&mut self, i: usize) -> WordRef { + WordRef::Const(BLAKE3_IV[i]) + } + + fn add3(&mut self, g: usize, half: usize, a: WordRef, b: WordRef, m_idx: usize) -> WordRef { + let base = cols::g_base(g) + if half == 0 { cols::G_A1 } else { cols::G_A2 }; + let cbase = cols::g_base(g) + + if half == 0 { + cols::G_A1_C + } else { + cols::G_A2_C + }; + let s = word_cols(base); + self.add3s.push(Add3Wire { + a, + b, + m: word_cols(cols::in_word(8 + m_idx, 0)), + s, + c1: cbase, + c2: cbase + 1, + }); + WordRef::Cols(s) + } + + fn add2(&mut self, g: usize, half: usize, a: WordRef, b: WordRef) -> WordRef { + let base = cols::g_base(g) + if half == 0 { cols::G_C1 } else { cols::G_C2 }; + let s = word_cols(base); + self.add2s.push(Add2Wire { a, b, s }); + WordRef::Cols(s) + } + + fn xor(&mut self, g: usize, slot: usize, a: WordRef, b: WordRef) -> WordRef { + let off = match slot { + 0 => cols::G_X1, + 1 => cols::G_X2, + 2 => cols::G_X3, + _ => cols::G_X4, + }; + let out = word_cols(cols::g_base(g) + off); + self.xors.push(XorWire { a, b, out }); + WordRef::Cols(out) + } + + fn rotr16(&mut self, w: WordRef) -> WordRef { + match w { + WordRef::Cols([b0, b1, b2, b3]) => WordRef::Cols([b2, b3, b0, b1]), + WordRef::Const(v) => WordRef::Const(v.rotate_right(16)), + } + } + fn rotr8(&mut self, w: WordRef) -> WordRef { + match w { + WordRef::Cols([b0, b1, b2, b3]) => WordRef::Cols([b1, b2, b3, b0]), + WordRef::Const(v) => WordRef::Const(v.rotate_right(8)), + } + } + + fn rot_shift(&mut self, g: usize, half: usize, w: WordRef) -> WordRef { + let base = cols::g_base(g) + if half == 0 { cols::G_R1 } else { cols::G_R2 }; + let y = word_cols(base + 8); + self.rots.push(RotWire { + input: w, + sll_lo: [base, base + 1], + sllc_lo: [base + 2, base + 3], + sll_hi: [base + 4, base + 5], + sllc_hi: [base + 6, base + 7], + y, + r: ROT_SHIFT_R[half], + }); + WordRef::Cols(y) + } + + fn feed_forward(&mut self, i: usize, vi: WordRef, vi8: WordRef, hi: WordRef) { + let out_lo = word_cols(cols::out_word(i, 0)); + let out_hi = word_cols(cols::out_word(i + 8, 0)); + self.xors.push(XorWire { + a: vi, + b: vi8, + out: out_lo, + }); + self.xors.push(XorWire { + a: vi8, + b: hi, + out: out_hi, + }); + } +} + +// ========================================================================= +// Value interpretation (u32 witness) +// ========================================================================= + +/// Everything the trace filler and the BITWISE collector need for one +/// compression, recorded cell-exactly in the same canonical order as +/// [`WireFlow`]. `xor_ops` carries (a, b) operand VALUES per XOR word — the +/// per-byte lookups are `(a_byte, b_byte)` in the same operand order the +/// senders use. +pub(crate) struct ValueFlow { + /// (s, c1, c2) per add3, canonical order. + pub add3s: Vec<(u32, u8, u8)>, + /// s per add2 (the carry is an expression, not a cell). + pub add2s: Vec, + /// (a, b, out) per XOR word, canonical order (Gs then feed-forward). + pub xors: Vec<(u32, u32, u32)>, + /// (sll_lo, sllc_lo, sll_hi, sllc_hi, y) per shift rotation. + pub rots: Vec<(u16, u16, u16, u16, u32)>, + /// The 16-word output. + pub out: [u32; 16], + + h: [u32; 8], + m: [u32; 16], + v12: [u32; 4], +} + +impl ValueFlow { + pub(crate) fn compute(h: &[u32; 8], m: &[u32; 16], t: u64, block_len: u32, flags: u32) -> Self { + let mut f = ValueFlow { + add3s: Vec::with_capacity(NUM_G * 2), + add2s: Vec::with_capacity(NUM_G * 2), + xors: Vec::with_capacity(NUM_G * 4 + 16), + rots: Vec::with_capacity(NUM_G * 2), + out: [0; 16], + h: *h, + m: *m, + v12: [t as u32, (t >> 32) as u32, block_len, flags], + }; + run_flow(&mut f); + f + } +} + +impl Blake3Flow for ValueFlow { + type Word = u32; + + fn input_h(&mut self, i: usize) -> u32 { + self.h[i] + } + fn input_v12(&mut self, j: usize) -> u32 { + self.v12[j] + } + fn iv_const(&mut self, i: usize) -> u32 { + BLAKE3_IV[i] + } + + fn add3(&mut self, _g: usize, _half: usize, a: u32, b: u32, m_idx: usize) -> u32 { + let m = self.m[m_idx]; + let wide = a as u64 + b as u64 + m as u64; + let s = wide as u32; + let carry = (wide >> 32) as u8; // 0, 1 or 2 + // Two summed carry bits: c1 + c2 = carry. + let (c1, c2) = match carry { + 0 => (0, 0), + 1 => (1, 0), + _ => (1, 1), + }; + self.add3s.push((s, c1, c2)); + s + } + + fn add2(&mut self, _g: usize, _half: usize, a: u32, b: u32) -> u32 { + let s = a.wrapping_add(b); + self.add2s.push(s); + s + } + + fn xor(&mut self, _g: usize, _slot: usize, a: u32, b: u32) -> u32 { + let out = a ^ b; + self.xors.push((a, b, out)); + out + } + + fn rotr16(&mut self, w: u32) -> u32 { + w.rotate_right(16) + } + fn rotr8(&mut self, w: u32) -> u32 { + w.rotate_right(8) + } + + fn rot_shift(&mut self, _g: usize, half: usize, w: u32) -> u32 { + let r = ROT_SHIFT_R[half]; + let xlo = w & 0xFFFF; + let xhi = w >> 16; + // xlo·2^r = SLLC_lo·2^16 + SLL_lo (and same for hi): Euclidean split. + let sll_lo = ((xlo << r) & 0xFFFF) as u16; + let sllc_lo = ((xlo << r) >> 16) as u16; + let sll_hi = ((xhi << r) & 0xFFFF) as u16; + let sllc_hi = ((xhi << r) >> 16) as u16; + // Recombine + halfword swap: Ylo = SLL_hi + SLLC_lo, Yhi = SLL_lo + SLLC_hi. + let ylo = sll_hi as u32 + sllc_lo as u32; + let yhi = sll_lo as u32 + sllc_hi as u32; + let y = ylo | (yhi << 16); + debug_assert_eq!(y, w.rotate_right(if r == 4 { 12 } else { 7 })); + self.rots.push((sll_lo, sllc_lo, sll_hi, sllc_hi, y)); + y + } + + fn feed_forward(&mut self, i: usize, vi: u32, vi8: u32, hi: u32) { + let lo = vi ^ vi8; + let hi_w = vi8 ^ hi; + self.xors.push((vi, vi8, lo)); + self.xors.push((vi8, hi, hi_w)); + self.out[i] = lo; + self.out[i + 8] = hi_w; + } +} + +// ========================================================================= +// Operation struct + trace generation +// ========================================================================= + +#[derive(Debug, Clone)] +pub struct Blake3Operation { + pub timestamp: u64, + pub state_addr: u64, + pub h: [u32; 8], + pub m: [u32; 16], + pub t: u64, + pub block_len: u32, + pub flags: u32, + /// Previous memory content of the 64-byte out region (for the Memw `old`). + pub old_out: [u8; 64], + /// The 16-word compression output (recomputed by the trace builder). + pub out: [u32; 16], +} + +/// Write a 32-bit word as 4 byte cells at `col..col+4`. +#[inline] +fn set_word_bytes(table: &mut T, row: usize, col: usize, w: u32) { + for b in 0..4 { + table.set_u64(row, col + b, ((w >> (8 * b)) & 0xFF) as u64); + } +} + +pub fn generate_blake3_trace( + ops: &[Blake3Operation], +) -> TraceTable { + let n = ops.len(); + let num_rows = n.next_power_of_two().max(4); + let mut trace = TraceTable::new_main( + crate::tables::types::zeroed_fe_vec(num_rows * cols::NUM_COLUMNS), + cols::NUM_COLUMNS, + 1, + ); + let table = &mut trace.main_table; + + for (row, op) in ops.iter().enumerate() { + table.set_dword_wl(row, cols::TIMESTAMP_0, op.timestamp); + table.set_dword_bl(row, cols::addr(0), op.state_addr); + + // Pointers ptr[k] = addr + 8k. + for k in 0..STATE_DWORDS { + let ptr = op + .state_addr + .checked_add(k as u64 * 8) + .expect("blake3 state address range must be validated by the executor"); + table.set_dword_hl(row, cols::ptr(k, 0), ptr); + } + + // Input words: h | m | t_lo t_hi len flags. + for i in 0..8 { + set_word_bytes(table, row, cols::in_word(i, 0), op.h[i]); + } + for i in 0..16 { + set_word_bytes(table, row, cols::in_word(8 + i, 0), op.m[i]); + } + set_word_bytes(table, row, cols::in_word(24, 0), op.t as u32); + set_word_bytes(table, row, cols::in_word(25, 0), (op.t >> 32) as u32); + set_word_bytes(table, row, cols::in_word(26, 0), op.block_len); + set_word_bytes(table, row, cols::in_word(27, 0), op.flags); + + // The mixing core, cell-exactly in canonical order. + let flow = ValueFlow::compute(&op.h, &op.m, op.t, op.block_len, op.flags); + debug_assert_eq!( + flow.out, op.out, + "trace-builder output must match the executor" + ); + + let mut a3 = flow.add3s.iter(); + let mut a2 = flow.add2s.iter(); + let mut xo = flow.xors.iter(); + let mut ro = flow.rots.iter(); + for g in 0..NUM_G { + let base = cols::g_base(g); + for half in 0..2 { + let (s_off, c_off, x_off, c2_off, x2_off, r_off) = if half == 0 { + ( + cols::G_A1, + cols::G_A1_C, + cols::G_X1, + cols::G_C1, + cols::G_X2, + cols::G_R1, + ) + } else { + ( + cols::G_A2, + cols::G_A2_C, + cols::G_X3, + cols::G_C2, + cols::G_X4, + cols::G_R2, + ) + }; + let &(s, c1, c2) = a3.next().expect("add3 count"); + set_word_bytes(table, row, base + s_off, s); + table.set_u64(row, base + c_off, c1 as u64); + table.set_u64(row, base + c_off + 1, c2 as u64); + + let &(_, _, x) = xo.next().expect("xor count"); + set_word_bytes(table, row, base + x_off, x); + + let &c = a2.next().expect("add2 count"); + set_word_bytes(table, row, base + c2_off, c); + + let &(_, _, x2) = xo.next().expect("xor count"); + set_word_bytes(table, row, base + x2_off, x2); + + let &(sll_lo, sllc_lo, sll_hi, sllc_hi, y) = ro.next().expect("rot count"); + table.set_u64(row, base + r_off, (sll_lo & 0xFF) as u64); + table.set_u64(row, base + r_off + 1, (sll_lo >> 8) as u64); + table.set_u64(row, base + r_off + 2, (sllc_lo & 0xFF) as u64); + table.set_u64(row, base + r_off + 3, (sllc_lo >> 8) as u64); + table.set_u64(row, base + r_off + 4, (sll_hi & 0xFF) as u64); + table.set_u64(row, base + r_off + 5, (sll_hi >> 8) as u64); + table.set_u64(row, base + r_off + 6, (sllc_hi & 0xFF) as u64); + table.set_u64(row, base + r_off + 7, (sllc_hi >> 8) as u64); + set_word_bytes(table, row, base + r_off + 8, y); + } + } + // Feed-forward outputs (the last 16 entries of flow.xors). + for i in 0..16 { + set_word_bytes(table, row, cols::out_word(i, 0), flow.out[i]); + } + // Previous content of the out region. + for b in 0..64 { + table.set_u64(row, cols::old_out(b), op.old_out[b] as u64); + } + + table.set_fe(row, cols::MU, FE::one()); + } + + // Padding rows: ptr[k][0] = 8k (all fit in the low halfword), matching the + // keccak pad idiom. μ = 0 gates every constraint and interaction. + for row in n..num_rows { + for k in 0..STATE_DWORDS { + table.set_u64(row, cols::ptr(k, 0), (k as u64) * 8); + } + } + + trace +} + +// ========================================================================= +// Bus interactions +// ========================================================================= + +/// Order groups: I/O (Ecall + reg-read + 22 Memw), then the mixing core's +/// ByteAlu XORs (canonical WireFlow order), then the shift AreBytes, then the +/// message/old-out/addr AreBytes, the alignment AND and the pointer IS_HALFs. +pub fn bus_interactions() -> Vec { + let syscall_lo = BLAKE3_SYSCALL_NUMBER & 0xFFFF_FFFF; + let syscall_hi = BLAKE3_SYSCALL_NUMBER >> 32; + let wires = WireFlow::build(); + let mut interactions = Vec::with_capacity(1400); + + let byte_bus_value = |b: ByteRef| -> BusValue { + match b { + ByteRef::Col(c) => BusValue::Packed { + start_column: c, + packing: Packing::Direct, + }, + ByteRef::Const(v) => BusValue::constant(v as u64), + } + }; + + // 1. ECALL receiver: [ts_lo, ts_hi, syscall_lo32, syscall_hi32]. + interactions.push(BusInteraction::receiver( + BusId::Ecall, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::TIMESTAMP_0, + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::TIMESTAMP_1, + packing: Packing::Direct, + }, + BusValue::constant(syscall_lo), + BusValue::constant(syscall_hi), + ], + )); + + // 2. MEMW read of register x10 binding the state address (keccak idiom): + // [old(8), is_register=1, base=20, value(8), ts(2), w2=1, w4=0, w8=0]. + { + let addr_word = |lo_byte: usize| -> BusValue { + BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: cols::addr(lo_byte), + }, + LinearTerm::Column { + coefficient: 256, + column: cols::addr(lo_byte + 1), + }, + LinearTerm::Column { + coefficient: 65536, + column: cols::addr(lo_byte + 2), + }, + LinearTerm::Column { + coefficient: 16777216, + column: cols::addr(lo_byte + 3), + }, + ]) + }; + let mut values = Vec::with_capacity(24); + values.push(addr_word(0)); + values.push(addr_word(4)); + for _ in 2..8 { + values.push(BusValue::constant(0)); + } + values.push(BusValue::constant(1)); // is_register + values.push(BusValue::constant(20)); // x10 → address 2*10 + values.push(BusValue::constant(0)); + values.push(addr_word(0)); + values.push(addr_word(4)); + for _ in 2..8 { + values.push(BusValue::constant(0)); + } + values.push(BusValue::Packed { + start_column: cols::TIMESTAMP_0, + packing: Packing::Direct, + }); + values.push(BusValue::Packed { + start_column: cols::TIMESTAMP_1, + packing: Packing::Direct, + }); + values.push(BusValue::constant(1)); // w2 (register) + values.push(BusValue::constant(0)); + values.push(BusValue::constant(0)); + interactions.push(BusInteraction::sender( + BusId::Memw, + Multiplicity::Column(cols::MU), + values, + )); + } + + // 3. MEMW per state dword: [old(8), is_register=0, addr(2), value(8), ts(2), + // w2=0, w4=0, w8=1]. Input dwords are pure reads (old = value = input + // bytes); output dwords write OUT over OLD_OUT. + for k in 0..STATE_DWORDS { + let addr_lo = BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: cols::ptr(k, 0), + }, + LinearTerm::Column { + coefficient: 65536, + column: cols::ptr(k, 1), + }, + ]); + let addr_hi = BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: cols::ptr(k, 2), + }, + LinearTerm::Column { + coefficient: 65536, + column: cols::ptr(k, 3), + }, + ]); + + // (old bytes, value bytes) column bases for this dword. + let (old_base, val_base): (Vec, Vec) = if k < IN_DWORDS { + let cols8: Vec = (0..8).map(|b| cols::in_word(2 * k, 0) + b).collect(); + (cols8.clone(), cols8) + } else { + let o = k - IN_DWORDS; + ( + (0..8).map(|b| cols::old_out(o * 8 + b)).collect(), + (0..8).map(|b| cols::out_word(2 * o, 0) + b).collect(), + ) + }; + + let mut values = Vec::with_capacity(24); + for &c in &old_base { + values.push(BusValue::Packed { + start_column: c, + packing: Packing::Direct, + }); + } + values.push(BusValue::constant(0)); // is_register + values.push(addr_lo); + values.push(addr_hi); + for &c in &val_base { + values.push(BusValue::Packed { + start_column: c, + packing: Packing::Direct, + }); + } + values.push(BusValue::Packed { + start_column: cols::TIMESTAMP_0, + packing: Packing::Direct, + }); + values.push(BusValue::Packed { + start_column: cols::TIMESTAMP_1, + packing: Packing::Direct, + }); + values.push(BusValue::constant(0)); + values.push(BusValue::constant(0)); + values.push(BusValue::constant(1)); // w8 + interactions.push(BusInteraction::sender( + BusId::Memw, + Multiplicity::Column(cols::MU), + values, + )); + } + + // 4. Mixing core + feed-forward: ByteAlu[XOR] per byte, canonical order. + for xw in &wires.xors { + for b in 0..4 { + interactions.push(BusInteraction::sender( + BusId::ByteAlu, + Multiplicity::Column(cols::MU), + vec![ + BusValue::constant(alu_op::XOR as u64), + byte_bus_value(xw.a.byte(b)), + byte_bus_value(xw.b.byte(b)), + BusValue::Packed { + start_column: xw.out[b], + packing: Packing::Direct, + }, + ], + )); + } + } + + // 5. Shift-halfword AreBytes: 4 pairs per rotation + // (SLL_lo, SLLC_lo, SLL_hi, SLLC_hi bytes). + for rw in &wires.rots { + for pair in [rw.sll_lo, rw.sllc_lo, rw.sll_hi, rw.sllc_hi] { + interactions.push(BusInteraction::sender( + BusId::AreBytes, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: pair[0], + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: pair[1], + packing: Packing::Direct, + }, + ], + )); + } + } + + // 6. Message AreBytes (m is never XORed — DESIGN §4.7/§7.5): 32 pairs. + for i in 0..16 { + for p in 0..2 { + interactions.push(BusInteraction::sender( + BusId::AreBytes, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::in_word(8 + i, 2 * p), + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::in_word(8 + i, 2 * p + 1), + packing: Packing::Direct, + }, + ], + )); + } + } + + // 7. OLD_OUT AreBytes: those bytes only ride the Memw bus; without a byte + // range check their packed linear combinations alias (same argument as the + // addr bytes in keccak.rs). + for p in 0..32 { + interactions.push(BusInteraction::sender( + BusId::AreBytes, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::old_out(2 * p), + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::old_out(2 * p + 1), + packing: Packing::Direct, + }, + ], + )); + } + + // 8. Address byte range checks (4 pairs) + alignment addr[0] & 7 = 0. + for i in 0..4 { + interactions.push(BusInteraction::sender( + BusId::AreBytes, + Multiplicity::Column(cols::MU), + vec![ + BusValue::Packed { + start_column: cols::addr(2 * i), + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::addr(2 * i + 1), + packing: Packing::Direct, + }, + ], + )); + } + interactions.push(BusInteraction::sender( + BusId::ByteAlu, + Multiplicity::Column(cols::MU), + vec![ + BusValue::constant(alu_op::AND as u64), + BusValue::Packed { + start_column: cols::addr(0), + packing: Packing::Direct, + }, + BusValue::constant(7), + BusValue::constant(0), + ], + )); + + // 9. IS_HALF range checks on the 22 pointers' halfwords. + for k in 0..STATE_DWORDS { + for hw in 0..4 { + interactions.push(BusInteraction::sender( + BusId::IsHalfword, + Multiplicity::Column(cols::MU), + vec![BusValue::Packed { + start_column: cols::ptr(k, hw), + packing: Packing::Direct, + }], + )); + } + } + + interactions +} + +// ========================================================================= +// Single-source constraint set +// ========================================================================= + +/// The BLAKE3 table's transition constraints (814 total): +/// - idx 0..44: 22 pointer `ADD` carry pairs (`ptr[k] = addr + 8k`, μ-gated); +/// - idx 44: μ·carry_1 = 0 — top-dword no-overflow (`addr + 168 = ptr[21]`); +/// - idx 45..333: all 96 add3 groups (sum identity + 2 carry booleanities); +/// - idx 333..429: all 96 add2 expression-carry booleanities; +/// - idx 429..813: all 96 rotations (2 shift identities + 2 recombine each). +/// NOTE the grouping is by op type across the whole row, NOT per G — G #g's +/// 16 constraints are scattered across the three bands. +/// - idx 813: `IS_BIT(MU)` — μ·(1−μ) = 0, ungated. The bus argument pins +/// μ to {0,1} indirectly (the Ecall receive anchors μ>0 rows to a CPU ecall +/// whose ECALL flag is IS_BIT; MEMW's width flags are boolean), but that is +/// an inter-table argument — this makes it local, matching ecsm/commit. +/// +/// All μ-gated, max degree 3 (the booleanities; identities are degree 2). +#[derive(Clone, Copy)] +pub struct Blake3Constraints; + +/// Word expression from a [`WordRef`]: b0 + 256·b1 + 2^16·b2 + 2^24·b3. +fn word_expr>( + b: &B, + w: &WordRef, +) -> B::Expr { + match w { + WordRef::Cols(c) => { + b.main(0, c[0]) + + b.main(0, c[1]) * b.const_base(256) + + b.main(0, c[2]) * b.const_base(65536) + + b.main(0, c[3]) * b.const_base(16777216) + } + WordRef::Const(v) => b.const_base(*v as u64), + } +} + +/// Halfword expression from 2 byte columns: b0 + 256·b1. +fn half_expr>( + b: &B, + c: &[usize; 2], +) -> B::Expr { + b.main(0, c[0]) + b.main(0, c[1]) * b.const_base(256) +} + +impl ConstraintSet for Blake3Constraints { + fn max_degree(&self) -> usize { + 3 + } + + fn eval>(&self, b: &mut B) { + use crate::constraints::templates::emit_add_pair; + + let wires = WireFlow::build(); + let mu = |b: &B| b.main(0, cols::MU); + + // idx 0..44: ptr[k] = addr + 8k (μ-gated carry pairs). + for k in 0..STATE_DWORDS { + emit_add_pair( + b, + k * 2, + &[cols::MU], + &AddOperand::from_dword_bl(cols::ADDR), + &AddOperand::constant((k * 8) as i64), + &AddOperand::from_dword_hl(cols::ptr(k, 0)), + ); + } + + // idx 44: top-dword no-overflow — μ·carry_1 of addr + 168 = ptr[21]. + let mut idx = STATE_DWORDS * 2; + { + let c256 = b.const_base(256); + let c65536 = b.const_base(65536); + let c16777216 = b.const_base(16777216); + let addr_lo = b.main(0, cols::addr(0)) + + b.main(0, cols::addr(1)) * c256.clone() + + b.main(0, cols::addr(2)) * c65536.clone() + + b.main(0, cols::addr(3)) * c16777216.clone(); + let addr_hi = b.main(0, cols::addr(4)) + + b.main(0, cols::addr(5)) * c256 + + b.main(0, cols::addr(6)) * c65536.clone() + + b.main(0, cols::addr(7)) * c16777216; + let last = STATE_DWORDS - 1; + let ptr_lo = + b.main(0, cols::ptr(last, 0)) + b.main(0, cols::ptr(last, 1)) * c65536.clone(); + let ptr_hi = b.main(0, cols::ptr(last, 2)) + b.main(0, cols::ptr(last, 3)) * c65536; + + let inv_2_32 = b.const_base(INV_SHIFT_32); + let off = b.const_base((8 * last) as u64); + let carry_0 = (addr_lo + off - ptr_lo) * inv_2_32.clone(); + let carry_1 = (addr_hi + carry_0 - ptr_hi) * inv_2_32; + let m = mu(b); + b.emit_base(idx, m * carry_1); + idx += 1; + } + + // Mixing core. Same canonical order as the wire builder records. + let two_32 = b.const_base(1u64 << 32); + let inv_2_32 = b.const_base(INV_SHIFT_32); + + // add3: μ·(a + b + m − s − 2^32·(c1+c2)) = 0; μ·ci·(1−ci) = 0. + for aw in &wires.add3s { + let a = word_expr(b, &aw.a); + let bb = word_expr(b, &aw.b); + let m_w = word_expr(b, &WordRef::Cols(aw.m)); + let s = word_expr(b, &WordRef::Cols(aw.s)); + let c1 = b.main(0, aw.c1); + let c2 = b.main(0, aw.c2); + let sum_id = a + bb + m_w - s - (c1.clone() + c2.clone()) * two_32.clone(); + let m = mu(b); + b.emit_base(idx, m * sum_id); + idx += 1; + let one = b.one(); + let m = mu(b); + b.emit_base(idx, m * c1.clone() * (one - c1)); + idx += 1; + let one = b.one(); + let m = mu(b); + b.emit_base(idx, m * c2.clone() * (one - c2)); + idx += 1; + } + + // add2: carry = (a + b − s)·2^−32; μ·carry·(1−carry) = 0. + for aw in &wires.add2s { + let a = word_expr(b, &aw.a); + let bb = word_expr(b, &aw.b); + let s = word_expr(b, &WordRef::Cols(aw.s)); + let carry = (a + bb - s) * inv_2_32.clone(); + let one = b.one(); + let m = mu(b); + b.emit_base(idx, m * carry.clone() * (one - carry)); + idx += 1; + } + + // Rotations: 2 shift identities + 2 recombine identities each. + for rw in &wires.rots { + let (xlo, xhi) = match &rw.input { + WordRef::Cols(c) => (half_expr(b, &[c[0], c[1]]), half_expr(b, &[c[2], c[3]])), + WordRef::Const(_) => unreachable!("shift inputs are always committed XOR outputs"), + }; + let sll_lo = half_expr(b, &rw.sll_lo); + let sllc_lo = half_expr(b, &rw.sllc_lo); + let sll_hi = half_expr(b, &rw.sll_hi); + let sllc_hi = half_expr(b, &rw.sllc_hi); + let ylo = half_expr(b, &[rw.y[0], rw.y[1]]); + let yhi = half_expr(b, &[rw.y[2], rw.y[3]]); + let two_r = b.const_base(1u64 << rw.r); + let two_16 = b.const_base(65536); + + // μ·(xlo·2^r − SLLC_lo·2^16 − SLL_lo) = 0 (and hi). + let m = mu(b); + b.emit_base( + idx, + m * (xlo * two_r.clone() - sllc_lo.clone() * two_16.clone() - sll_lo.clone()), + ); + idx += 1; + let m = mu(b); + b.emit_base( + idx, + m * (xhi * two_r - sllc_hi.clone() * two_16 - sll_hi.clone()), + ); + idx += 1; + // μ·(Ylo − SLL_hi − SLLC_lo) = 0; μ·(Yhi − SLL_lo − SLLC_hi) = 0. + let m = mu(b); + b.emit_base(idx, m * (ylo - sll_hi - sllc_lo)); + idx += 1; + let m = mu(b); + b.emit_base(idx, m * (yhi - sll_lo - sllc_hi)); + idx += 1; + } + + // idx 813: IS_BIT(MU) — ungated booleanity, degree 2. See the struct + // doc for why this is emitted even though the bus argument already + // pins μ indirectly. + crate::constraints::templates::emit_is_bit(b, idx, cols::MU, None); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The value interpretation must reproduce the executor's compression — + /// same function the canonical oracle vectors validate. + #[test] + fn value_flow_matches_executor() { + use executor::vm::instruction::execution::blake3_compress_6round; + let h: [u32; 8] = core::array::from_fn(|i| 0x9E3779B9u32.wrapping_mul(i as u32 + 1)); + let m: [u32; 16] = core::array::from_fn(|i| 0x85EBCA6Bu32.wrapping_mul(i as u32 + 7)); + let t = 0x0123_4567_89AB_CDEFu64; + let (bl, fl) = (64u32, 11u32); + let flow = ValueFlow::compute(&h, &m, t, bl, fl); + assert_eq!(flow.out, blake3_compress_6round(&h, &m, t, bl, fl)); + } + + /// Canonical op counts: 96 add3s, 96 add2s, 96 rotations, 192+16 XORs. + #[test] + fn wire_flow_counts() { + let w = WireFlow::build(); + assert_eq!(w.add3s.len(), NUM_G * 2); + assert_eq!(w.add2s.len(), NUM_G * 2); + assert_eq!(w.rots.len(), NUM_G * 2); + assert_eq!(w.xors.len(), NUM_G * 4 + 16); + // Every output column lands exactly once, and inside the row. + use std::collections::HashSet; + let mut seen = HashSet::new(); + let mut claim = |c: usize| { + assert!(c < cols::NUM_COLUMNS, "column {c} out of range"); + assert!(seen.insert(c), "column {c} written twice"); + }; + for aw in &w.add3s { + for c in aw.s { + claim(c); + } + claim(aw.c1); + claim(aw.c2); + } + for aw in &w.add2s { + for c in aw.s { + claim(c); + } + } + for xw in &w.xors { + for c in xw.out { + claim(c); + } + } + for rw in &w.rots { + for c in rw + .sll_lo + .iter() + .chain(&rw.sllc_lo) + .chain(&rw.sll_hi) + .chain(&rw.sllc_hi) + .chain(&rw.y) + { + claim(*c); + } + } + // 48 G-blocks × 60 cells + 64 out bytes, all distinct. + assert_eq!(seen.len(), NUM_G * cols::G_SIZE + 64); + } +} diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 781bb02b0..4add10639 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -185,6 +185,10 @@ pub struct CpuOperation { pub ecall_keccak: bool, /// For KeccakPermute ECALLs: state address from x10. pub keccak_state_addr: u64, + /// Whether this ECALL is a Blake3Compress syscall. + pub ecall_blake3: bool, + /// For Blake3Compress ECALLs: state address from x10. + pub blake3_state_addr: u64, /// Whether this ECALL is an ECSM (elliptic-curve scalar multiply) syscall pub ecall_ecsm: bool, @@ -231,6 +235,9 @@ impl CpuOperation { let ecall_keccak = f.ecall && log.src1_val == executor::vm::instruction::execution::KECCAK_SYSCALL_NUMBER; let keccak_state_addr = if ecall_keccak { log.src2_val } else { 0 }; + let ecall_blake3 = + f.ecall && log.src1_val == executor::vm::instruction::execution::BLAKE3_SYSCALL_NUMBER; + let blake3_state_addr = if ecall_blake3 { log.src2_val } else { 0 }; // The ECSM operand addresses (x10/x11/x12) are recovered from the register state // in the trace builder. let ecall_ecsm = @@ -252,6 +259,8 @@ impl CpuOperation { commit_count, ecall_keccak, keccak_state_addr, + ecall_blake3, + blake3_state_addr, decode, timestamp, ..Default::default() @@ -352,6 +361,8 @@ impl CpuOperation { commit_count, ecall_keccak, keccak_state_addr, + ecall_blake3, + blake3_state_addr, ecall_ecsm, } } diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index 0a86e4149..784f83a96 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -22,6 +22,7 @@ pub mod types; pub mod bitwise; +pub mod blake3; pub mod branch; pub mod bytewise; pub mod commit; diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 5ec9fa566..9fc183e54 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -40,6 +40,7 @@ use stark::storage_mode::StorageMode; use stark::trace::TraceTable; use super::bitwise::{self, BitwiseOperation, BitwiseOperationType}; +use super::blake3::{self, Blake3Operation}; use super::branch::{self, BranchOperation}; use super::bytewise; use super::commit::{self, CommitOperation}; @@ -546,6 +547,7 @@ fn collect_ops_from_cpu( Vec, Vec, Vec, + Vec, Vec, Vec, Vec, @@ -557,6 +559,7 @@ fn collect_ops_from_cpu( let mut bitwise_ops = Vec::with_capacity(cpu_ops.len() * 4); let mut commit_ops = Vec::new(); let mut keccak_ops = Vec::new(); + let mut blake3_ops = Vec::new(); let mut cpu32_ops = Vec::new(); let mut ecsm_ops = Vec::new(); let mut ecdas_ops = Vec::new(); @@ -645,6 +648,60 @@ fn collect_ops_from_cpu( }); } + // Collect Blake3Compress ECALL operations + if op.ecall_blake3 { + let state_addr = op.blake3_state_addr; + // 14 input dwords: h | m | t | (block_len, flags), LE words. + let mut words = [0u32; 28]; + for k in 0..14usize { + let dword_addr = state_addr + .checked_add(k as u64 * 8) + .expect("blake3 state address range must be validated by the executor"); + let mut dw = 0u64; + for b in 0..8 { + let byte_addr = dword_addr + .checked_add(b as u64) + .expect("blake3 state address range must be validated by the executor"); + let (byte_val, _ts) = memory_state.read_byte(byte_addr); + dw |= (byte_val as u64) << (b * 8); + } + words[2 * k] = dw as u32; + words[2 * k + 1] = (dw >> 32) as u32; + } + let h: [u32; 8] = words[0..8].try_into().unwrap(); + let m: [u32; 16] = words[8..24].try_into().unwrap(); + let t = (words[24] as u64) | ((words[25] as u64) << 32); + let block_len = words[26]; + let flags = words[27]; + let out = executor::vm::instruction::execution::blake3_compress_6round( + &h, &m, t, block_len, flags, + ); + // Previous content of the out region, read BEFORE the write ops + // below advance memory_state. + let mut old_out = [0u8; 64]; + for (b, byte) in old_out.iter_mut().enumerate() { + let byte_addr = state_addr + .checked_add(112 + b as u64) + .expect("blake3 state address range must be validated by the executor"); + let (v, _ts) = memory_state.read_byte(byte_addr); + *byte = v; + } + let blake3_memw_ops = + collect_blake3_memw_ops(op, &words, &out, memory_state, register_state); + memw.extend_ops(blake3_memw_ops); + blake3_ops.push(Blake3Operation { + timestamp: op.timestamp, + state_addr, + h, + m, + t, + block_len, + flags, + old_out, + out, + }); + } + // Collect ECSM ecall operations (memory I/O + the two table row sets) if op.ecall_ecsm { let (ecsm_memw, ecsm_op, ecdas_rows) = @@ -706,6 +763,7 @@ fn collect_ops_from_cpu( bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -1417,6 +1475,79 @@ fn collect_keccak_memw_ops( memw_ops } +/// Collect MEMW operations for a Blake3Compress ECALL. +/// +/// One register read of x10 plus 22 dword ops at the call's timestamp: the 14 +/// input dwords are pure reads (old = value = the input bytes, re-written at +/// `ts` like a LOAD), the 8 output dwords write the compression output over +/// the previous content. +fn collect_blake3_memw_ops( + op: &CpuOperation, + words: &[u32; 28], + out: &[u32; 16], + memory_state: &mut MemoryState, + register_state: &mut RegisterState, +) -> Vec { + let ts = op.timestamp; + let state_addr = op.blake3_state_addr; + let mut memw_ops = Vec::with_capacity(23); // 1 register read + 22 dword ops + + // Read register x10 to bind state_addr (same as keccak:c:read_addr). + { + let reg_value = pack_register_value(state_addr); + let reg_addr = 2 * 10u64; // x10 -> address 20 + let (_old_val, old_ts) = register_state.read(10); + let old_timestamps = [old_ts, old_ts, 0, 0, 0, 0, 0, 0]; + let memw_op = MemwOperation::new(true, reg_addr, reg_value, ts, 2, true) + .with_old(reg_value, old_timestamps); + memw_ops.push(memw_op); + register_state.write(10, state_addr, ts); + } + + for k in 0..22usize { + let dword_addr = state_addr + .checked_add(k as u64 * 8) + .expect("blake3 state address range must be validated by the executor"); + + // The dword's new value: input dwords re-write their own bytes, output + // dwords write the compression output. + let dw = if k < 14 { + (words[2 * k] as u64) | ((words[2 * k + 1] as u64) << 32) + } else { + let o = k - 14; + (out[2 * o] as u64) | ((out[2 * o + 1] as u64) << 32) + }; + let mut value_bytes = [0u32; 8]; + for (b, byte) in value_bytes.iter_mut().enumerate() { + *byte = ((dw >> (b * 8)) & 0xFF) as u32; + } + + let mut old_bytes = [0u32; 8]; + let mut old_timestamps = [0u64; 8]; + for b in 0..8 { + let byte_addr = dword_addr + .checked_add(b as u64) + .expect("blake3 state address range must be validated by the executor"); + let (old_val, old_ts) = memory_state.read_byte(byte_addr); + old_bytes[b] = old_val as u32; + old_timestamps[b] = old_ts; + } + + let memw_op = MemwOperation::new(false, dword_addr, value_bytes, ts, 8, true) + .with_old(old_bytes, old_timestamps); + memw_ops.push(memw_op); + + for (b, &val) in value_bytes.iter().enumerate() { + let byte_addr = dword_addr + .checked_add(b as u64) + .expect("blake3 state address range must be validated by the executor"); + memory_state.write_byte(byte_addr, val as u8, ts); + } + } + + memw_ops +} + /// /// From spec memw.md: /// - MEMW-C4 through MEMW-C7: old_timestamp[i] < timestamp (based on width) @@ -2334,6 +2465,97 @@ pub(crate) fn collect_bitwise_from_ecdas(ops: &[ecdas::EcdasOperation]) -> Vec Vec { + let mut ops = Vec::new(); + + for bop in blake3_ops { + let state_addr = bop.state_addr; + + // Alignment: addr[0] & 7 = 0. + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::ByteAluAnd, + (state_addr & 0xFF) as u8, + 7, + )); + + // Addr byte range checks: (addr[2i], addr[2i+1]) pairs. + for i in 0..4 { + let lo = ((state_addr >> (2 * i * 8)) & 0xFF) as u8; + let hi = ((state_addr >> ((2 * i + 1) * 8)) & 0xFF) as u8; + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::AreBytes, + lo, + hi, + )); + } + + // IS_HALF for the 22 pointers' halfwords. + for k in 0..blake3::STATE_DWORDS { + let ptr = state_addr + .checked_add(k as u64 * 8) + .expect("blake3 state address range must be validated by the executor"); + for shift in [0, 16, 32, 48] { + let half = ((ptr >> shift) & 0xFFFF) as u16; + ops.push(BitwiseOperation::halfword( + BitwiseOperationType::IsHalf, + (half & 0xFF) as u8, + ((half >> 8) & 0xFF) as u8, + )); + } + } + + // Mixing core + feed-forward, in the senders' canonical order. + let flow = blake3::ValueFlow::compute(&bop.h, &bop.m, bop.t, bop.block_len, bop.flags); + for &(a, b, _out) in &flow.xors { + for byte in 0..4 { + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::ByteAluXor, + ((a >> (8 * byte)) & 0xFF) as u8, + ((b >> (8 * byte)) & 0xFF) as u8, + )); + } + } + for &(sll_lo, sllc_lo, sll_hi, sllc_hi, _y) in &flow.rots { + for hw in [sll_lo, sllc_lo, sll_hi, sllc_hi] { + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::AreBytes, + (hw & 0xFF) as u8, + (hw >> 8) as u8, + )); + } + } + + // Message AreBytes: (byte 2p, byte 2p+1) of each m word. + for m in bop.m { + for p in 0..2 { + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::AreBytes, + ((m >> (16 * p)) & 0xFF) as u8, + ((m >> (16 * p + 8)) & 0xFF) as u8, + )); + } + } + + // OLD_OUT AreBytes pairs. + for p in 0..32 { + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::AreBytes, + bop.old_out[2 * p], + bop.old_out[2 * p + 1], + )); + } + } + + ops +} + /// Collect BITWISE lookups generated by the keccak chips. /// /// The keccak round chip sends BYTE_ALU and ARE_BYTES interactions (the θ/ρ @@ -2761,6 +2983,9 @@ pub struct Traces { /// KECCAK_RC precomputed round constant table (32 rows) pub keccak_rc: TraceTable, + /// BLAKE3 6-round compression table (one row per compression call) + pub blake3: TraceTable, + /// ECSM core table (one row per scalar-multiplication ecall) pub ecsm: TraceTable, @@ -2801,6 +3026,7 @@ struct CollectedOps { dvrm_ops: Vec<(DvrmOperation, bool)>, commit_ops: Vec, keccak_ops: Vec, + blake3_ops: Vec, // Auxiliary ALU / memory / CPU32 dispatch chips (driven by the CPU ALU/MEMORY dispatch). eq_ops: Vec, bytewise_ops: Vec, @@ -2860,6 +3086,7 @@ fn collect_all_ops( mut bitwise_ops: Vec, commit_ops: Vec, keccak_ops: Vec, + blake3_ops: Vec, cpu32_ops: Vec, ecsm_ops: Vec, ecdas_ops: Vec, @@ -2999,6 +3226,7 @@ fn collect_all_ops( dvrm_ops, commit_ops, keccak_ops, + blake3_ops, eq_ops, bytewise_ops, store_ops, @@ -3042,6 +3270,7 @@ fn build_traces( dvrm_ops, commit_ops, keccak_ops, + blake3_ops, eq_ops, bytewise_ops, store_ops, @@ -3121,6 +3350,7 @@ fn build_traces( Box::new(|h| h.add_ops(&collect_bitwise_from_memw_aligned(&memw_aligned_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_commit(&commit_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_keccak(&keccak_ops))), + Box::new(|h| h.add_ops(&collect_bitwise_from_blake3(&blake3_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_ecsm(&ecsm_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_ecdas(&ecdas_ops))), Box::new(|h| add_padding_byte_checks(h, num_padding_rows)), @@ -3390,6 +3620,7 @@ fn build_traces( .collect(); keccak_rnd::generate_keccak_rnd_trace(&keccak_rnd_ops) }; + let gen_blake3 = || blake3::generate_blake3_trace(&blake3_ops); let gen_keccak_rc = || { let mut keccak_rc_trace = keccak_rc::generate_keccak_rc_trace(); keccak_rc::update_multiplicities(&mut keccak_rc_trace, keccak_ops.len()); @@ -3417,6 +3648,7 @@ fn build_traces( (None, None, None, None); let (mut commit_slot, mut keccak_slot, mut keccak_rnd_slot, mut keccak_rc_slot) = (None, None, None, None); + let mut blake3_slot = None; let (mut pages_slot, mut register_slot, mut halt_slot) = (None, None, None); let (mut eqs_slot, mut bytewises_slot, mut stores_slot, mut cpu32s_slot) = (None, None, None, None); @@ -3453,6 +3685,7 @@ fn build_traces( spawn_into!(keccak_slot, gen_keccak); spawn_into!(keccak_rnd_slot, gen_keccak_rnd); spawn_into!(keccak_rc_slot, gen_keccak_rc); + spawn_into!(blake3_slot, gen_blake3); spawn_into!(commit_slot, gen_commit); spawn_into!(register_slot, gen_register); spawn_into!(halt_slot, gen_halt); @@ -3480,6 +3713,7 @@ fn build_traces( keccak_slot = Some(gen_keccak()); keccak_rnd_slot = Some(gen_keccak_rnd()); keccak_rc_slot = Some(gen_keccak_rc()); + blake3_slot = Some(gen_blake3()); pages_slot = Some(gen_pages()); register_slot = Some(gen_register()); halt_slot = Some(gen_halt()); @@ -3515,6 +3749,7 @@ fn build_traces( let keccak_trace = keccak_slot.expect(PHASE5_RAN); let keccak_rnd_trace = keccak_rnd_slot.expect(PHASE5_RAN); let keccak_rc_trace = keccak_rc_slot.expect(PHASE5_RAN); + let blake3_trace = blake3_slot.expect(PHASE5_RAN); #[allow(unused_mut)] let (mut pages, page_configs) = pages_slot.expect(PHASE5_RAN); #[allow(unused_mut)] @@ -3587,6 +3822,7 @@ fn build_traces( commit: commit_trace, keccak: keccak_trace, keccak_rnd: keccak_rnd_trace, + blake3: blake3_trace, keccak_rc: keccak_rc_trace, ecsm: ecsm_trace, ecdas: ecdas_trace, @@ -3840,6 +4076,7 @@ impl Traces { pub fn total_field_elements(&self) -> u64 { use super::bitwise::NUM_PRECOMPUTED_COLS as BITWISE_PRECOMPUTED; use super::bitwise::cols::NUM_COLUMNS as BITWISE_COLS; + use super::blake3::cols::NUM_COLUMNS as BLAKE3_COLS; use super::branch::cols::NUM_COLUMNS as BRANCH_COLS; use super::bytewise::cols::NUM_COLUMNS as BYTEWISE_COLS; use super::commit::cols::NUM_COLUMNS as COMMIT_COLS; @@ -3888,6 +4125,7 @@ impl Traces { keccak, keccak_rnd, keccak_rc, + blake3, ecsm, ecdas, memw_registers, @@ -3943,6 +4181,7 @@ impl Traces { total += (keccak.num_rows() * KECCAK_COLS) as u64; total += (keccak_rnd.num_rows() * KECCAK_RND_COLS) as u64; total += (keccak_rc.num_rows() * (KECCAK_RC_COLS - KECCAK_RC_PRECOMPUTED)) as u64; + total += (blake3.num_rows() * BLAKE3_COLS) as u64; for t in eqs { total += (t.num_rows() * EQ_COLS) as u64; } @@ -3992,6 +4231,7 @@ impl Traces { let n_keccak = aux_cols(super::keccak::bus_interactions().len()); let n_keccak_rnd = aux_cols(super::keccak_rnd::bus_interactions().len()); let n_keccak_rc = aux_cols(super::keccak_rc::bus_interactions().len()); + let n_blake3 = aux_cols(super::blake3::bus_interactions().len()); let n_eq = aux_cols(super::eq::bus_interactions().len()); let n_bytewise = aux_cols(super::bytewise::bus_interactions().len()); let n_store = aux_cols(super::store::bus_interactions().len()); @@ -4018,6 +4258,7 @@ impl Traces { keccak, keccak_rnd, keccak_rc, + blake3, ecsm, ecdas, memw_registers, @@ -4073,6 +4314,7 @@ impl Traces { total += (keccak.num_rows() * n_keccak) as u64; total += (keccak_rnd.num_rows() * n_keccak_rnd) as u64; total += (keccak_rc.num_rows() * n_keccak_rc) as u64; + total += (blake3.num_rows() * n_blake3) as u64; for t in eqs { total += (t.num_rows() * n_eq) as u64; } @@ -4357,6 +4599,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4375,6 +4618,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4468,6 +4712,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, @@ -4482,6 +4727,7 @@ impl Traces { bitwise_ops, commit_ops, keccak_ops, + blake3_ops, cpu32_ops, ecsm_ops, ecdas_ops, diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index d7969612f..73a567864 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -39,6 +39,9 @@ use crate::tables::bitwise::{ BitwiseOperation, BitwiseOperationType, bus_interactions as bitwise_bus_interactions, cols as bitwise_cols, }; +use crate::tables::blake3::{ + Blake3Constraints, bus_interactions as blake3_bus_interactions, cols as blake3_cols, +}; use crate::tables::branch::{ BranchConstraints, bus_interactions as branch_bus_interactions, cols as branch_cols, }; @@ -954,6 +957,18 @@ pub fn create_keccak_air(proof_options: &ProofOptions) -> ConcreteVmAir ConcreteVmAir { + build_air( + blake3_cols::NUM_COLUMNS, + blake3_bus_interactions(), + proof_options, + 1, + Blake3Constraints, + "BLAKE3", + ) +} + /// Create KECCAK_RND AIR with pi constraints and bus interactions. pub fn create_keccak_rnd_air(proof_options: &ProofOptions) -> ConcreteVmAir { build_air( diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index ffe9071b2..9c265d102 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1102,6 +1102,55 @@ fn test_prove_elfs_keccak_multi_call() { ); } +#[test] +fn test_prove_elfs_blake3() { + let _ = env_logger::builder().is_test(true).try_init(); + + let elf_bytes = crate::test_utils::asm_elf_bytes("test_blake3"); + let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); + let executor = + executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + let result = executor.run().expect("Failed to run program"); + + // The guest seeds the 14 input dwords with k+1, compresses, copies out over + // m and compresses again. Cross-check the committed output against a direct + // replay of the executor's compression function. + use executor::vm::instruction::execution::blake3_compress_6round; + let words: [u32; 28] = core::array::from_fn(|i| { + let dw = (i / 2 + 1) as u64; + if i % 2 == 0 { + dw as u32 + } else { + (dw >> 32) as u32 + } + }); + let h: [u32; 8] = words[0..8].try_into().unwrap(); + let m: [u32; 16] = words[8..24].try_into().unwrap(); + let t = (words[24] as u64) | ((words[25] as u64) << 32); + let (block_len, flags) = (words[26], words[27]); + let out1 = blake3_compress_6round(&h, &m, t, block_len, flags); + let out2 = blake3_compress_6round(&h, &out1, t, block_len, flags); + let expected_bytes: Vec = out2.iter().flat_map(|w| w.to_le_bytes()).collect(); + + assert_eq!( + result.return_values.memory_values, expected_bytes, + "committed output must match two chained 6-round compressions" + ); + + // Must use from_elf_and_logs (stack RAM needs PAGE tables, like keccak). + let mut traces = + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + assert_eq!( + traces.public_output_bytes, + result.return_values.memory_values + ); + + assert!( + prove_and_verify_vm_minimal(&elf, &mut traces), + "blake3 prove/verify failed" + ); +} + #[test] fn test_prove_elfs_ecsm() { let _ = env_logger::builder().is_test(true).try_init(); diff --git a/scripts/gen_blake3_bench.sh b/scripts/gen_blake3_bench.sh new file mode 100755 index 000000000..ce640739d --- /dev/null +++ b/scripts/gen_blake3_bench.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# +# gen_blake3_bench.sh — generate + compile a blake3-saturated guest. +# +# The guest seeds a 176-byte BLAKE3 state region (layout: h[4 dwords] | m[8] | +# t[1] | len,flags[1] | out[8]), fires the BLAKE3 6-round compression ecall N +# times IN PLACE on that region, commits the 64-byte output and halts. +# +# The calls are deliberately NOT chained (out is not copied over m): chaining +# costs an 8-dword copy loop = ~82 of ~87 cycles per compression, and those +# copy cycles become CPU/MEMW rows that dilute the very table being measured. +# The prover's cost per ecall is identical either way — the executor runs every +# call, and no layer dedupes rows (timestamps differ per call) — so dropping +# the chain buys a ~5-cycle loop body and a >90% blake3-saturated trace, the +# same shape as gen_keccak_bench.sh. (The e2e correctness test, which is about +# values rather than cost, does chain: prover/src/tests test_prove_elfs_blake3.) +# +# The BLAKE3 table commits ONE row per compression (fully unrolled layout), so +# padding-flush sweep points are simply powers of two: N = 2^k. At ~5 +# cycles/compression a 2^17-row table costs ~0.7M cycles — inside one 2^20 +# epoch; use --epoch-size-log2 21 from N = 2^18 up. +# +# ABI (executor/src/vm/instruction/execution.rs BLAKE3_SYSCALL_NUMBER): +# a7 = u64::MAX - 2, written as the sign-extended -3 +# a0 = 8-byte-aligned pointer to the 176-byte region +# +# Usage: scripts/gen_blake3_bench.sh N OUT.elf +# Honors CLANG / ASM_CFLAGS / ASM_LDFLAGS like the Makefile's asm rule. + +set -euo pipefail + +N="${1:?usage: gen_blake3_bench.sh N out.elf}" +OUT="${2:?usage: gen_blake3_bench.sh N out.elf}" + +if ! [[ "$N" =~ ^[0-9]+$ ]] || [ "$N" -lt 1 ]; then + echo "gen_blake3_bench.sh: N must be a positive integer, got '$N'" >&2 + exit 1 +fi + +CLANG="${CLANG:-clang}" +ASM_CFLAGS="${ASM_CFLAGS:---target=riscv64 -march=rv64im -mabi=lp64}" +ASM_LDFLAGS="${ASM_LDFLAGS:--fuse-ld=lld -nostdlib -Wl,-e,main}" + +if ! command -v "$CLANG" >/dev/null 2>&1; then + echo "gen_blake3_bench.sh: '$CLANG' not found; run 'make deps' or set CLANG=..." >&2 + exit 1 +fi + +SRC="$(mktemp "${TMPDIR:-/tmp}/blake3_bench.XXXXXX.s")" +trap 'rm -f "$SRC"' EXIT + +cat > "$SRC" < + +One row per compression call, fully unrolled: 6 rounds × 8 G-functions in +SSA form. The message schedule is a compile-time index permutation (the +`sched` array in `run_flow`, composed from `BLAKE3_MSG_PERMUTATION`), so +every round references the 16 original committed message words — there is +no state or message handoff between rows. I/O follows the KECCAK core idiom: an `ECALL` receiver binds +(timestamp, syscall number), a `MEMW` register read binds the `x10` +pointer, and 22 per-dword `MEMW` operations carry the reads and writes. + +Key constraint-design decisions (full rationale: +`thoughts/blake3/blake3-chip/DESIGN.md`, deltas in `IMPLEMENTATION.md`): + +- every eval constraint is gated by the multiplicity column $mu$, and the + maximum constraint degree *including* the $times mu$ factor is 3; +- 3-operand adds commit *two summed carry bits* with an explicit sum + identity (a ternary carry would be degree 4 after gating); +- 2-operand adds use an expression carry (no committed cell) with a + $mu$-gated booleanity; +- `rotr16`/`rotr8` are free byte relabels; `rotr12`/`rotr7` are inline + $mu$-gated Euclidean shift identities whose soundness rests on the + tight $[0, 2^16)$ bound of the `SLL` halfwords ($2^16$ is invertible + mod $p$); +- every add/shift output feeds a downstream `BYTE_ALU[XOR]` lookup, which + is its only byte range check; the message words, the previous + out-region content and the address bytes are never XOR-consumed and + carry explicit `ARE_BYTES` checks instead. + +The chip's wiring is single-sourced: the compression dataflow is written +once in `prover/src/tables/blake3.rs` (`run_flow`) and interpreted both as +column wiring (constraints + bus senders) and as the u32 witness (trace +fill + lookup multiplicities), so the two cannot diverge structurally. + +== Formalized constraints + +#render_constraint_table(chip, config, groups: "io") +#render_constraint_table(chip, config, groups: "addr") +#render_constraint_table(chip, config, groups: "range") +#render_constraint_table(chip, config, groups: "mu") + += Verification evidence + +The design was taken to a z3-gated model *before* the Rust implementation +(`thoughts/blake3/blake3-chip/z3_blake_verify.py`): the G quarter-round +and the init/feed-forward layout are UNSAT under free inputs, five +negative controls and two field-level bound-necessity controls are SAT, +and the concrete 6- and 7-round pipelines reproduce the oracle's pinned +vectors. Two independent transcription audits +(`thoughts/blake3/TRANSCRIPTION-AUDIT.md`, +`GATE-TRANSCRIPTION-AUDIT.md`) checked the gate against the oracle. The +Rust chip is additionally pinned by the 10 canonical 6-round vectors at +the syscall level and by an end-to-end prove+verify of chained +compressions. + += The 6-round assumption + +*A6R.* The BLAKE3 compression function restricted to 6 rounds is +collision-resistant and suitable as a 2-to-1 compression for Merkle +hashing and as a PRF for Fiat–Shamir, in the same sense the full 7-round +function is believed to be. (Precedent: KangarooTwelve's reduced-round +Keccak. Best public cryptanalysis of BLAKE3 reaches far fewer rounds; the +margin removed here is one round of seven.) + +*External review (2026-08).* The round-count choice was reviewed with +external symmetric-cryptography experts consulted by the project: removing +*one* round (7 → 6) was judged comfortable; removing *two* (7 → 5) was +explicitly not. Accordingly, 6 rounds is the endorsed floor. Variants +below 6 rounds are not formally ruled out, but they are not available on +the project's own authority: adopting one would require the external +experts to study the reduced-round margin specifically — a dedicated +cryptanalytic review, not an engineering or configuration decision. + +Any use of #blake3 as a Merkle or transcript hash *invokes this +assumption*. The z3 gate proves the chip computes 6-round BLAKE3 +correctly; it neither proves nor addresses whether 6 rounds are secure. + +*The assumption-free alternative.* The chip design is round-parameterised; +a 7-round instantiation (standard BLAKE3 compression, bit-compatible with +official parent-node merges) costs roughly 10–12% more per merge +end-to-end and requires no assumption beyond standard BLAKE3. The 6-round +variant is the primary internal target per the review above; the 7-round +variant is the interoperability / zero-assumption fallback. If both are +instantiated they are distinct chips with distinct ECALL numbers. + += Cost + +Measured on the CPU bench box (32 cores, blowup 2, single-epoch +continuations): ≈5,473 compressions/s at ≥#raw("2^17") table rows, ≈7,194 +committed cell-equivalents per compression end-to-end (≈5,316 table-only) +— ≈12× the keccak-f permutation per 2-to-1 merge at equal wall time and +memory. Details and methodology: PR \#903. diff --git a/spec/book.typ b/spec/book.typ index 8bf8612af..7faf8ef1b 100644 --- a/spec/book.typ +++ b/spec/book.typ @@ -49,6 +49,7 @@ ("commit.typ", [`COMMIT` chip], ), ("sha256.typ", [`SHA256` accelerator], ), ("keccak.typ", [`KECCAK` accelerator], ), + ("blake3.typ", [`BLAKE3_6R` accelerator], ), )) ) ) diff --git a/spec/src/blake3.toml b/spec/src/blake3.toml new file mode 100644 index 000000000..bb04388c0 --- /dev/null +++ b/spec/src/blake3.toml @@ -0,0 +1,345 @@ +# BLAKE3_6R — the 6-round internal-variant BLAKE3 compression accelerator. +# +# ⚠ NORMATIVE SOURCE NOTE. This spec documents the shipped chip +# (`prover/src/tables/blake3.rs`); the chip's wiring is single-sourced in Rust +# (`run_flow` interpreted as columns and as witness) and formally gated by the +# z3 model in `thoughts/blake3/blake3-chip/z3_blake_verify.py`. Where this file +# and those artifacts disagree, THEY are normative and this file has a bug. +# Cross-checked totals at spec-writing time: 3,219 main columns, 1,397 +# interactions, 814 constraints, max degree 3 (incl. the ×μ gating factor). +# +# One row = one compression call: 6 rounds × 8 G-functions fully unrolled in +# SSA form. Message schedule = the literal per-round index table `SCHED` below +# (permute^r of the identity under MSG_PERMUTATION = +# [2,6,3,10,7,0,4,13,1,11,12,5,9,14,15,8]); round r position i consumes +# original message word SCHED[r][i]. +# +# Security: this chip computes 6-round BLAKE3, NOT the standard 7-round +# function. Its use as a Merkle / Fiat–Shamir hash rests on the named +# assumption A6R (see blake3.typ). No external system will ever agree on +# these digests. + +name = "BLAKE3" + +# ------------------------------------------------------------------------- +# Inputs (read from memory at addr .. addr+112; see the ECALL ABI) +# ------------------------------------------------------------------------- + +[[variables.input]] +name = "timestamp" +type = "DWordWL" +desc = "timestamp at which the compression is performed" +pad = 0 + +[[variables.input]] +name = "addr" +type = "DWordBL" +desc = "8-aligned base address of the 176-byte state region (h|m|t|len,flags|out)" +pad = 0 + +[[variables.input]] +name = "h" +type = [["Byte", 4], 8] +desc = "input chaining value h[0..8], 8 little-endian u32 words" +pad = 0 + +[[variables.input]] +name = "m" +type = [["Byte", 4], 16] +desc = "message block m[0..16] = left_cv ‖ right_cv for a 2-to-1 merge" +pad = 0 + +[[variables.input]] +name = "t_lo" +type = ["Byte", 4] +desc = "low u32 of the 64-bit counter t → v[12]" +pad = 0 + +[[variables.input]] +name = "t_hi" +type = ["Byte", 4] +desc = "high u32 of the 64-bit counter t → v[13] (split order is load-bearing)" +pad = 0 + +[[variables.input]] +name = "block_len" +type = ["Byte", 4] +desc = "input byte count of this block → v[14]" +pad = 0 + +[[variables.input]] +name = "flags" +type = ["Byte", 4] +desc = "domain-separation flags → v[15]" +pad = 0 + +# ------------------------------------------------------------------------- +# Outputs (written to memory at addr+112 .. addr+176) +# ------------------------------------------------------------------------- + +[[variables.output]] +name = "out" +type = [["Byte", 4], 16] +desc = "full 16-word compression output; the truncated CV is out[0..8]" +pad = 0 + +# ------------------------------------------------------------------------- +# Auxiliary +# ------------------------------------------------------------------------- + +[[variables.auxiliary]] +name = "state_ptr" +type = ["DWordHL", 22] +desc = "per-dword pointers state_ptr[k] = addr + 8k over the 22-dword region" +pad = ["*", 8, ["arr", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]] + +[[variables.auxiliary]] +name = "old_out" +type = [["Byte", 8], 8] +desc = "previous memory content of the out region, dword o = bytes old_out[o]; the MEMW writes' old value" +pad = 0 + +# Per-G SSA cells, 48 instances (g = 8r + j, round r ∈ 0..6, call j ∈ 0..8). +# Each G block: A1(4B) c1,c2(2 bits) X1(4B) C1(4B) X2(4B) +# R1: SLL_lo(2B) SLLC_lo(2B) SLL_hi(2B) SLLC_hi(2B) Y1(4B) +# A2(4B) c3,c4(2 bits) X3(4B) C2(4B) X4(4B) +# R2: same shape as R1 +# = 56 byte cells + 4 carry bits = 60 cells; column base = 210 + 60g +# (see prover/src/tables/blake3.rs `cols` for the exact offsets). + +[[variables.auxiliary]] +name = "g_add3_out" +type = [[["Byte", 4], 2], 48] +desc = "A1, A2 per G: the two 3-operand add outputs (v[a] after each half)" +pad = 0 + +[[variables.auxiliary]] +name = "g_add3_carry" +type = [[["Bit", 2], 2], 48] +desc = "two summed carry bits per 3-operand add: carry = c1 + c2 ∈ {0,1,2}" +pad = 0 + +[[variables.auxiliary]] +name = "g_xor_out" +type = [[["Byte", 4], 4], 48] +desc = "X1..X4 per G: the four 32-bit XOR outputs (rotr16/rotr8 are free byte relabels of X1/X3)" +pad = 0 + +[[variables.auxiliary]] +name = "g_add2_out" +type = [[["Byte", 4], 2], 48] +desc = "C1, C2 per G: the two 2-operand add outputs (v[c]); carries are expressions, not cells" +pad = 0 + +[[variables.auxiliary]] +name = "g_rot" +type = [[["Byte", 12], 2], 48] +desc = "per rotation (rotr12 then rotr7): SLL_lo, SLLC_lo, SLL_hi, SLLC_hi (2 B each) and the output word Y (4 B)" +pad = 0 + +[[variables.multiplicity]] +name = "μ" +type = "Bit" +desc = "1 on real rows, 0 on padding; gates every constraint and interaction; pinned boolean by an ungated IS_BIT" +pad = 0 + +# ------------------------------------------------------------------------- +# Constants +# ------------------------------------------------------------------------- + +[[constants]] +name = "IV" +desc = "BLAKE3 IV[0..4] (SHA-256 IV words), inlined into round-0 arithmetic — not columns" +value = ["arr", 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A] + +[[constants]] +name = "SCHED" +desc = "per-round message-schedule index table: round r position i reads m[SCHED[r][i]]; SCHED[r] = permute^r(identity), MSG_PERMUTATION = [2,6,3,10,7,0,4,13,1,11,12,5,9,14,15,8]; the trailing permute after round 5 is never consumed" +value = ["arr", + ["arr", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + ["arr", 2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8], + ["arr", 3, 4, 10, 12, 13, 2, 7, 14, 6, 5, 9, 0, 11, 15, 8, 1], + ["arr", 10, 7, 12, 9, 14, 3, 13, 15, 4, 0, 11, 2, 5, 8, 1, 6], + ["arr", 12, 13, 9, 11, 15, 10, 14, 8, 7, 2, 5, 3, 0, 1, 6, 4], + ["arr", 9, 14, 11, 5, 8, 12, 15, 1, 13, 3, 0, 10, 2, 6, 4, 7], +] + +[[constants]] +name = "G_INDICES" +desc = "the (a,b,c,d) working-state slots of the 8 G-calls per round: 4 column mixes then 4 diagonal mixes" +value = ["arr", + ["arr", 0, 4, 8, 12], ["arr", 1, 5, 9, 13], ["arr", 2, 6, 10, 14], ["arr", 3, 7, 11, 15], + ["arr", 0, 5, 10, 15], ["arr", 1, 6, 11, 12], ["arr", 2, 7, 8, 13], ["arr", 3, 4, 9, 14], +] + +# ------------------------------------------------------------------------- +# Constraint groups. All eval constraints are μ-gated; padding rows are +# all-zero (except the keccak-idiom state_ptr pad) with μ = 0. +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +# Constraint groups. +# +# ⚠ SCOPE. This file machine-formalizes the chip's I/O AND RANGE surface — +# the ECALL binding, the x10 register read, all 22 MEMW dword operations, +# the pointer arithmetic, and every explicit ARE_BYTES/IS_HALF/IS_BIT — i.e. +# exactly the surface the z3 gate does NOT model (DESIGN.md §1.1: "the gate +# cannot check this", §7 items 4/5/10). The unrolled 6-round MIXING CORE +# (96 add3 sum identities + carry booleanities, 96 add2 expression-carry +# booleanities, 96 inline rotation identity groups, 832 BYTE_ALU[XOR] +# lookups wired per the SSA dataflow) is NOT re-formalized here: its +# normative sources are the single-source Rust dataflow +# (prover/src/tables/blake3.rs `run_flow`, interpreted once as columns and +# once as witness) and the z3 gate that proves that dataflow equal to the +# reference function under free inputs. Totals for cross-checking: 814 +# constraints, 1,397 interactions, max degree 3 including ×μ. +# ------------------------------------------------------------------------- + +[[constraint_groups]] +name = "io" + +# ECALL receive: [timestamp, -3 as u64 = 2^64 - 3] +[[constraints.io]] +kind = "interaction" +tag = "ECALL" +input = ["timestamp", ["cast", ["-", ["^", 2, 64], 3], "DWordWL"]] +multiplicity = ["-", "μ"] + +# MEMW register read of x10 binding addr (keccak:c:read_addr idiom) +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [1, ["cast", ["*", 2, 10], "DWordWL"], "addr", "timestamp", 1, 0, 0] +output = "addr" +multiplicity = "μ" +ref = "blake3:c:read_addr" + +# The 22 dword MEMW operations at `timestamp`, per region. Input dwords +# (k < 14) are reads in the combined read+write encoding: old = value = the +# input words, timestamps advance. Output dwords (k >= 14) write `out` over +# `old_out`. + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", "k"], "DWordWL"], ["idx", "h", ["*", 2, "k"]], ["idx", "h", ["+", ["*", 2, "k"], 1]], "timestamp", 0, 0, 1] +output = ["arr", ["idx", "h", ["*", 2, "k"]], ["idx", "h", ["+", ["*", 2, "k"], 1]]] +iters = [["k", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:load_h" + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", ["+", "k", 4]], "DWordWL"], ["idx", "m", ["*", 2, "k"]], ["idx", "m", ["+", ["*", 2, "k"], 1]], "timestamp", 0, 0, 1] +output = ["arr", ["idx", "m", ["*", 2, "k"]], ["idx", "m", ["+", ["*", 2, "k"], 1]]] +iters = [["k", 0, 7]] +multiplicity = "μ" +ref = "blake3:c:load_m" + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", 12], "DWordWL"], "t_lo", "t_hi", "timestamp", 0, 0, 1] +output = ["arr", "t_lo", "t_hi"] +multiplicity = "μ" +ref = "blake3:c:load_t" + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", 13], "DWordWL"], "block_len", "flags", "timestamp", 0, 0, 1] +output = ["arr", "block_len", "flags"] +multiplicity = "μ" +ref = "blake3:c:load_len_flags" + +[[constraints.io]] +kind = "interaction" +tag = "MEMW" +input = [0, ["cast", ["idx", "state_ptr", ["+", "o", 14]], "DWordWL"], ["idx", "out", ["*", 2, "o"]], ["idx", "out", ["+", ["*", 2, "o"], 1]], "timestamp", 0, 0, 1] +output = ["idx", "old_out", "o"] +iters = [["o", 0, 7]] +multiplicity = "μ" +ref = "blake3:c:store_out" + +[[constraint_groups]] +name = "addr" + +# state_ptr[k] = addr + 8k via the shared ADD template (μ-gated carry pair); +# the top dword k = 21 additionally forbids wraparound (addr + 168 < 2^64). +[[constraints.addr]] +kind = "template" +tag = "ADD" +input = [["cast", "addr", "DWordWL"], ["cast", ["*", 8, "k"], "DWordWL"]] +output = ["cast", ["idx", "state_ptr", "k"], "DWordWL"] +iters = [["k", 0, 21]] +ref = "blake3:c:state_ptr" + +# alignment: addr[0] & 7 = 0 +[[constraints.addr]] +kind = "interaction" +tag = "BYTE_ALU" +input = [0, ["idx", "addr", 0], 7, 0] +multiplicity = "μ" +ref = "blake3:c:alignment" + +# addr byte range checks — without them the addr_lo/addr_hi linear +# combinations alias non-byte encodings (keccak:c:range_addr rationale). +[[constraints.addr]] +kind = "interaction" +tag = "ARE_BYTES" +input = [["idx", "addr", ["*", 2, "i"]], ["idx", "addr", ["+", ["*", 2, "i"], 1]]] +iters = [["i", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:range_addr" + +[[constraints.addr]] +kind = "interaction" +tag = "IS_HALF" +input = [["idx", ["cast", ["idx", "state_ptr", "k"], "DWordHL"], "hw"]] +iters = [["k", 0, 21], ["hw", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:range_state_ptr" + +[[constraint_groups]] +name = "range" + +# m is never XOR-consumed — explicit byte checks (DESIGN §4.7 / §7.5). +[[constraints.range]] +kind = "interaction" +tag = "ARE_BYTES" +input = [["idx", ["idx", "m", "i"], ["*", 2, "p"]], ["idx", ["idx", "m", "i"], ["+", ["*", 2, "p"], 1]]] +iters = [["i", 0, 15], ["p", 0, 1]] +multiplicity = "μ" +ref = "blake3:c:range_m" + +# old_out rides only the MEMW bus — same aliasing argument as the address. +[[constraints.range]] +kind = "interaction" +tag = "ARE_BYTES" +input = [["idx", ["idx", "old_out", "o"], ["*", 2, "p"]], ["idx", ["idx", "old_out", "o"], ["+", ["*", 2, "p"], 1]]] +iters = [["o", 0, 7], ["p", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:range_old_out" + +# The four shift halfwords of each inline rotation (SLL_lo, SLLC_lo, +# SLL_hi, SLLC_hi — bytes 0..8 of each rotation block). The tight SLL +# bounds are load-bearing for the rotation identities (DESIGN §4.2). +[[constraints.range]] +kind = "interaction" +tag = "ARE_BYTES" +input = [["idx", ["idx", ["idx", "g_rot", "g"], "half"], ["*", 2, "p"]], ["idx", ["idx", ["idx", "g_rot", "g"], "half"], ["+", ["*", 2, "p"], 1]]] +iters = [["g", 0, 47], ["half", 0, 1], ["p", 0, 3]] +multiplicity = "μ" +ref = "blake3:c:range_rot" + +[[constraint_groups]] +name = "mu" + +# μ boolean, ungated — the ECALL receive anchors μ>0 rows to a CPU ecall +# whose ECALL flag is boolean; this constraint makes the argument local. +[[constraints.mu]] +kind = "template" +tag = "IS_BIT" +input = ["μ"] +ref = "blake3:c:range_mu" diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index 7165dff81..6c0ef5271 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -29,6 +29,10 @@ pub enum SyscallNumbers { #[cfg(target_arch = "riscv64")] const KECCAK_SYSCALL_NUMBER: usize = usize::MAX - 1; +/// Syscall number for the BLAKE3 6-round compression accelerator (u64::MAX - 2). +#[cfg(target_arch = "riscv64")] +const BLAKE3_SYSCALL_NUMBER: usize = usize::MAX - 2; + /// Syscall number for the ECSM secp256k1 scalar-multiply accelerator (-11 as usize). #[cfg(target_arch = "riscv64")] const ECSM_SYSCALL_NUMBER: usize = usize::MAX - 10; @@ -165,6 +169,30 @@ pub fn keccak_permute(_state: &mut [u64; 25]) { unimplemented!("syscalls are only implemented for riscv64 targets"); } +#[cfg(target_arch = "riscv64")] +/// BLAKE3 **6-round** compression via the accelerator (internal variant — NOT +/// standard 7-round BLAKE3; see `thoughts/blake3/blake3-chip/DESIGN.md`). +/// +/// `state` is the 176-byte region as 22 dwords: `h[8 words] | m[16 words] | +/// t | (block_len, flags) | out[16 words]`, all words little-endian, two per +/// dword. The accelerator reads dwords 0..14 and writes `out` to dwords 14..22. +/// Using `[u64; 22]` guarantees the 8-byte alignment the ecall requires. +pub fn blake3_compress_6round(state: &mut [u64; 22]) { + unsafe { + asm!( + "ecall", + in("a0") state.as_mut_ptr(), + in("a7") BLAKE3_SYSCALL_NUMBER, + ) + } +} + +#[cfg(not(target_arch = "riscv64"))] +/// BLAKE3 6-round compression via the accelerator (internal variant). +pub fn blake3_compress_6round(_state: &mut [u64; 22]) { + unimplemented!("syscalls are only implemented for riscv64 targets"); +} + #[cfg(target_arch = "riscv64")] /// Compute `xR = (k·G)_x` on secp256k1 via the ECSM accelerator. All values are 32-byte /// little-endian. Requires `0 < k < N` and a canonical valid `xG` curve coordinate. diff --git a/thoughts/blake3/.gitignore b/thoughts/blake3/.gitignore new file mode 100644 index 000000000..7fcf8256c --- /dev/null +++ b/thoughts/blake3/.gitignore @@ -0,0 +1,4 @@ +target/ +Cargo.lock +__pycache__/ +venv/ diff --git a/thoughts/blake3/GATE-TRANSCRIPTION-AUDIT.md b/thoughts/blake3/GATE-TRANSCRIPTION-AUDIT.md new file mode 100644 index 000000000..5d8c7ba17 --- /dev/null +++ b/thoughts/blake3/GATE-TRANSCRIPTION-AUDIT.md @@ -0,0 +1,471 @@ +# Transcription audit — does the BLAKE3 gate assert more than the design delivers? + +Adversarial, one-directional audit of `blake3-chip/z3_blake_verify.py` against +(a) the oracle `blake3-oracle/blake3_ref.py` and (b) the constraint system +`blake3-chip/DESIGN.md` specifies. Branch `spike/blake3-recovered`. + +Only one direction is dangerous. A model **weaker** than the object yields a +spurious SAT — a false alarm. A model **stronger** yields UNSAT on an object +that is genuinely forgeable — false assurance — and no positive control can see +it, because an honest witness satisfies a correct model and an over-strong model +equally well. The three positive controls and the 35-vector oracle anchor are +blind to exactly this. + +Reproduce: `python3 audit_gate_transcription.py` (~4 min, 73 checks) or +`--slow` (+ the gate's own BV UNSATs, ~8 min). Every check is paired with a +tamper that must make it fail; a check that does not bite is itself reported as +a failure. No file outside this audit was modified — tampers are applied to +in-memory copies and reverted. + +--- + +## Verdict + +**One over-strong assertion, and it is the load-bearing one: the gate declares +byte-ness rather than deriving it.** Every committed cell in the model is a +`BitVec(…, 8)`, so the range check that DESIGN §4.3/§4.4/§5/§7.4 make the whole +soundness argument rest on is an *axiom of the model*, not something the model +can observe being present or absent. The gate proves the same UNSAT for the +designed chip and for a chip with **no range checks at all**. + +I checked whether that gap is live. It is not, today: I traced every SSA value +in `build_g` and `build_compress` mechanically and the design's premise holds — +all 288 add/shift outputs of a 6-round compression are consumed by a `ByteAlu` +XOR, for ROUNDS ∈ {1,2,6,7}. So the gate's UNSAT is *correct*, for a reason the +gate does not model. It would stay UNSAT under three specific deviations, one of +them invited by DESIGN §1.1 itself. + +Transcription (a) — `bref_*` vs the oracle — is **exact**, mechanically, on every +element the brief named. + +| # | severity | what | +|---|---|---| +| **F1** | **high** | **Model stronger.** Byte-ness is the `BitVec(…,8)` declaration. The gate cannot distinguish a chip that range-checks an add output from one that does not, nor a chip with §4.7's 32 message `AreBytes` sends from one without them. The premise holds today; nothing in the gate would notice it becoming false. | +| **F2** | medium | **Model weaker / evidence over-stated.** The width audit's `DROP SLL bound → SAT` control is run on a *single-halfword fragment*. Composed with the second identity, the recombine and the downstream byte check, dropping one `SLL` bound is **UNSAT** and dropping both is forgeable at **exactly one input**, `X = 0xFFFFFFFF`. §7.3's stated mechanism is also backwards. | +| **F3** | medium | The "covers every G, hence every round" chaining argument (§9, MAIN 0) **is** the free-range-check argument in disguise, and the gate models neither. It is prose on both counts. | +| F4 | low | μ, padding rows, the degree ledger, and the bus layer are not carried at all. Scope, not error — but §7.1's "μ-gate every eval constraint" and §4.8's degree table are cited as gate-backed and are not. | +| F5 | cosmetic | Three documentation defects (§7.3's mechanism, §4.8's recombine degree row, §3-vs-§4.3 on whether add2 commits a carry column). | + +Along the way, two results the gate did not have: + +* The rotation is pinned to `rotr12` / `rotr7` **for all 2³² inputs in the + field**, not just in BV and not just at one concrete halfword. +* The necessary *and sufficient* `AreBytes` set for a rotation is **one of + `{SLL_lo, SLL_hi}`**. The two `SLLC` bounds and one of the two `SLL` bounds + are not load-bearing — 3 of the 4 sends per rotation, i.e. 288 of the design's + 1,250 sends per compression. + +--- + +## F1 — the free range check is declared, not derived + +This is the claim DESIGN §7.4 flags as soundness-critical and the one the gate is +cited to discharge: + +> §4.3 `s`'s bytes are range-checked **free** by the next XOR that consumes `s` +> §5 Every committed add/shift word is an operand of a later XOR ⇒ its bytes +> are byte-range-checked for free by that `ByteAlu` lookup. +> §7.4 **Every add/shift output must actually feed a downstream XOR** (its only +> range check). If a future refactor reorders so an add output is *last* +> with no XOR consumer, add an explicit AreBytes or the carry argument is +> unsound. + +The model's answer to "where does `s ∈ [0,2^32)` come from" is +`z3_blake_verify.py:133-134`: + +```python +def fresh_word(self): + return [self._fresh(8) for _ in range(4)] +``` + +and `:128-129`, `self._fresh(w=8)` → `BitVec(f"{tag}_v{n}", w)`. `add2` +(`:176-184`) and `add3` (`:186-197`) both begin `s = self.fresh_word()`. So `s` +is four 8-bit bitvectors *by construction*. The audit confirms it mechanically: +every cell the model creates is width 8, and every constraint it emits is `=` +(xor / sum / shift / recombine) or `or` (carry booleanity) — 1,408 and 288 +respectively for a 6-round compression. **There is no range-check object in the +model to be present or absent.** + +**In fairness, the gate says it is doing this.** The `Circuit` class header +(`:118-119`) reads *"A `word` is a list of 4 free 8-bit BVs … Byte width == the +ByteAlu/AreBytes range-check contract"*, and the module docstring (`:18-21`) +lists `ByteAlu[XOR]` and `AreBytes` under "Chip contracts assumed". So this is a +**documented assumption, not a hidden one** — unlike the EC audit's F2, nothing +here is detected from a comment. Inside the class, "AreBytes" occurs only in +those two comments (`:119`, `:212`); the only place the gate *models* an +`AreBytes` bound is `field_shift_bound` (`:388`), and only for the isolated +fragment F2 is about. + +The problem is that the equation "byte width == the range-check contract" is +applied **uniformly to every column**, including the ones for which the design +must separately arrange the contract (§4.2's and §4.7's explicit sends, §5's +downstream-XOR argument) — and DESIGN §9 then cites the gate's UNSAT as +establishing what the comment assumed. The gate's own docstring (`:5`) states +the method as "Every committed column of the designed chip is a FREE bitvector"; +it is a free *byte*, and byte-ness is the property under proof. + +### Which gate rows this invalidates + +| DESIGN §9 row | as written | what it actually establishes | +|---|---|---| +| MAIN 0, one G, free inputs, **UNSAT** | "the quarter-round … is **correctly & tightly constrained**" | correctly & tightly constrained **given that its inputs and its add outputs are byte-range-checked** — which is §7.4/§7.5's obligation on the Rust, not a result | +| MAIN 1, rounds=0, **UNSAT** | "`v` layout … and the feed-forward are correct" | same caveat for `h`, `t_lo/t_hi`, `block_len`, `flags` | +| §9 "Proven (symbolic, all inputs): the G quarter-round … hence, by the chaining argument, the full N-round compression" | unconditional | conditional on the same premise — see F3 | + +### Where the missing range check actually bites + +The gate declares byte-ness on *every* cell, so I checked, per SSA class, +whether the range check is load-bearing at all. Three of the six classes do not +need one; the model's blanket declaration hides that distinction too. + +| SSA class | per G | model's source of byte-ness | chip's source (DESIGN) | is it load-bearing? | +|---|---:|---|---|---| +| `add2`/`add3` output `s` (A1, C1, A2, C2) | 4 words | `BitVec(…,8)` | downstream `ByteAlu` (§4.3/§4.4/§5) | **YES** — without it the chip may commit the **unreduced** sum | +| `ByteAlu` XOR output (X1..X4) | 4 words | `BitVec(…,8)` | the lookup itself | n/a — pinned by contract | +| `SLL_lo`, `SLL_hi` | 2 halfwords/rot | `BitVec(…,8)` | explicit `AreBytes` (§4.2) | **YES, but only one of the two** (F2) | +| `SLLC_lo`, `SLLC_hi` | 2 halfwords/rot | `BitVec(…,8)` | explicit `AreBytes` (§4.2) | **no** — proved | +| rotation output `Y` (B1, B2) | 1 word/rot | `BitVec(…,8)` | downstream `ByteAlu` (§5) | **no** — the two recombine identities pin its *value* even with free field cells | +| message `m` | 16 words | `BitVec(…,8)` | explicit `AreBytes`, §4.7 | **YES** for cell-binding | +| `h`, `t_lo/t_hi/block_len/flags` | 12 words | `BitVec(…,8)` | XOR consumers (§4.7) | for cell-binding | + +Each row is an executable check in `audit_gate_transcription.py` §B/§C. + +### The forgery it hides — construction + +**(1) An add output without its ByteAlu consumer.** This is verbatim the §7.4 +deviation. Model the design's add in the field instead of in BV: `s` is four +Goldilocks cells, `carry ∈ {0,1}`, and the only constraint is §4.3's sum +identity. + +``` +a = b = 0x80000000 +honest : carry = 1, s = 0 cells [0, 0, 0, 0] +forged : carry = 0, s = 2^32 cells [2^32, 0, 0, 0] +``` + +Both satisfy `a + b ≡ s + 2^32·carry (mod p)` and the booleanity. The add no +longer computes mod 2^32; the chip's `v[c]` is off by exactly 2^32, and the +error propagates through every downstream add. `audit_gate_transcription.py` §C +runs it and reports both `add2` and `add3` FORGEABLE with the range check +removed, PINNED with it, symbolically over all operands. The BV model reports +UNSAT in both worlds because there is nothing to remove. + +**(2) §4.7's message range check — the one explicit input `AreBytes` in the whole +design (32 sends).** The gate models `m = [cir.fresh_word() for _ in range(16)]` +(`:332`), i.e. 64 declared bytes. Drop the sends and the 64 cells are free field +elements entering only through `wval(M) = Σ m_i·2^{8i}`, so the chip binds the +*value*, not the bytes: + +``` +m0 honest : [0x9A, 0x00, 0x13, 0x7F] +m0 forged : [0x19A, p−1, 0x13, 0x7F] same value mod p +``` + +Every constraint in the circuit is satisfied identically and the compression +output is bit-identical. The chip proves "the compression of these 64 cells" and +there are `p^3` cell-vectors per message word. Whether this becomes a Merkle +collision depends on the caller — for the §1.1 internal bus the receive tuple +carries the cells, so a byte-constrained counterparty pins them, and for the +§1.2 memory variant MEMW does. **The two gaps compose**: the gate models neither +the `AreBytes` nor the bus, so the obligation is discharged by nothing. + +**(3) The CV-only feed-forward trim — the deviation the design invites.** +`h[0..4]` land in round-0 `a` slots, and G uses `v[a]` only as an **add +operand**; it is never XORed. Their sole `ByteAlu` consumer is the *upper* +feed-forward half `out[i+8] = v[i+8] ⊕ h[i]`. The audit measures this: consumer +counts `[1,1,1,1,2,2,2,2]`, and for `h[0]` that single consumer is op #577 of 592 +— i.e. the feed-forward, not a round XOR. + +DESIGN §1.1 says: *"CV-only call sites read `out[0:8]`; the chip always produces +all 16 (the XOF root needs them)"*. For the internal 2-to-1 Merkle variant — the +primary target — an implementer dropping `out[8:16]` saves 32 committed cells and +32 sends (≈80 cell-equiv, 1.6%) and removes `h[0..4]`'s only range check. §7.4 +does not cover it, because it speaks of *add/shift outputs* and `h` is an input. +The gate reports the same UNSAT. + +### Fix + +Give the model a range-check object. Concretely: have `Circuit` allocate cells +that are *not* byte-bounded by default and add an explicit `are_bytes(word)` / +`byte_alu_xor()` that imposes the bound, so that "this word has no consumer" is +representable and shows up as SAT. That is a rewrite of the model into the field +(z3 `Int` mod p, as `audit_gate_transcription.py` §C does per-op) — but a cheaper +90% is available: keep BV and add the **structural** check this audit implements, +asserting that every `add`/`rotr` output word appears as a `xor` operand and that +`m` is explicitly range-checked. Twenty lines, and it is the check the design's +§7.4 actually asks for. + +--- + +## F2 — the width audit's shift control is run on a fragment + +`field_shift_bound` (`:380-395`) models **one halfword identity in isolation**: +`in_hw·2^r ≡ SLLC·2^16 + SLL (mod p)`, `SLLC ∈ [0,2^16)`, `SLL` unbounded, at one +concrete `in_hw = 0x9C3A` and one `r = 9`. It reports SAT and §9 renders that as + +> audit: **DROP `SLL` bound** (field neg ctrl) | **SAT** | without it the +> rotation is forgeable + +The chip does not contain that fragment. It contains **two** shift identities, +**two** recombine identities, and `Y` byte-range-checked by the downstream XOR. +Composing them (`audit_gate_transcription.py` §C2, symbolic over all 2³² inputs) +gives a different picture: + +| bounds kept | r=4 (rotr12) | r=9 (rotr7) | +|---|---|---| +| all four | PINNED | PINNED | +| `SLL_lo` only | **PINNED** | **PINNED** | +| `SLL_hi` only | **PINNED** | **PINNED** | +| `SLLC_lo` and/or `SLLC_hi`, no `SLL` | FORGEABLE | FORGEABLE | +| none | FORGEABLE | FORGEABLE | + +All 32 configurations were also checked non-vacuous (the honest witness +satisfies each). So: + +* **dropping one `SLL` bound is not exploitable at all** — the gate's control + claims it is; +* **dropping both** is exploitable at **exactly one input**. Enumerated + exhaustively, for both `r`: + +``` +X = 0xFFFFFFFF honest Y = 0xFFFFFFFF forged Y = 0x00000000 +SLL_lo = SLL_hi = p − 2^r (i.e. honest − 2^16 as a field element) +SLLC_lo = SLLC_hi = 2^r (i.e. honest + 1) +``` + +Every other `X` is UNSAT. The defence is still necessary — a prover can grind an +intermediate XOR output to `0xFFFFFFFF` cheaply, and there are 96 rotation slots +per compression — but "forgeable" at one point is not what the control shows, and +the control shows it for a chip that does not exist. + +**§7.3's mechanism is backwards.** It says *"dropping it makes the rotation +forgeable (a wrong `SLL` admits a **large field SLLC**)"*. `SLLC` is bounded to +`[0,2^16)` by its own `AreBytes` and stays small in the forgery (`2^r`); it is +`SLL` that goes large (`p − 2^r`). The witness above is the counterexample to the +prose, not to the conclusion. + +**Cost consequence, flagged not pursued:** §4.2 spends 4 `AreBytes` sends per +rotation. One suffices in this composed model. At 96 rotations that is 288 of +the design's ~1,250 sends per compression → ≈432 aux cells ≈ **8.6% of the 5,030 +cell-equiv budget**. Before acting on that, note it depends on `Y` being +byte-checked by its consumer — i.e. on F1's premise — and on `2^{-16} mod p` +being large, which is exactly the kind of implicit structural fact this audit +exists to distrust. It wants its own gate row, not a code change. + +By contrast `field_add_carry` (`:398-413`) **is** faithful: dropping the carry +booleanity is forgeable even with `s` byte-range-checked (verified composed), and +its UNSAT direction holds for all `(a,b,m)`, not just the one concrete triple it +tests. Both width-audit positives were re-derived symbolically and hold +universally. + +--- + +## F3 — "covers every G, hence every round" is the same argument as the free range check + +§9's MAIN 0 row carries the whole default run: + +> **covers every G, hence every round** (a round is a fixed composition of 8 +> G-calls). + +MAIN 0 proves: *for all byte-valued `v[a],v[b],v[c],v[d],mx,my`, the G's four +outputs equal `bref_g`*. Composing it needs each G's **inputs** to be +byte-valued, which holds because they are the previous G's outputs, which are +byte-valued because of the downstream-XOR range check. **The chaining argument +and the free-range-check argument are one argument.** The gate models neither: +the chaining is prose in §9, and the range check is the `BitVec(…,8)` +declaration. + +What *is* checkable, and what this audit checks mechanically because the gate +does not: + +* `build_round` calls `build_g` on the 8 quadruples of `G_CALLS`, in order, all + 7 rounds — compared against the quadruples recovered by instrumenting the + **oracle's** `round_fn`, not against the gate's own constant. +* `build_compress` feeds every G the original message column under `permute^r` + — all 7 rounds × 8 calls = **56 index pairs**, compared against the oracle's + permutation composition. Both tampers (a swapped `MSG_PERMUTATION` entry, a + swapped `mx/my` in `G_CALLS[5]`) are detected. +* Every G quadruple has four distinct state indices, the 8 calls touch each of + the 16 slots exactly twice and consume each message index exactly once — so + MAIN 0's `a,b,c,d = 0,1,2,3` instance really is general. + +README finding 2 ("in Rust, 48 G instances are emitted separately and a wrong +column index in instance #37 is not covered") stands and is out of scope: there +is no Rust. + +--- + +## F4 — what the model does not carry at all + +Not errors; scope. Listed because DESIGN cites the gate for some of them. + +| DESIGN claim | modelled? | consequence | +|---|---|---| +| §4.5 / §7.1 "every eval constraint is μ-gated; padding rows all-zero" | **no** — no μ variable exists; the docstring says "here mu=1 (a real row), so mu drops out" | exact for a live row. The gate says nothing about padding rows, so §7.1 is unbacked. Note the *ungated* system is strictly **stronger** as a system over all rows — it is only the single-live-row scope that makes this safe. | +| §4.8 degree ledger; the O1 (a)-vs-(c) decision | **no** — the model has no degree notion | `check_g` would be equally UNSAT for the rejected ternary-carry option (a). (a) is rejected for degree, not soundness, so this is harmless — but §4.8 is not gate-backed. | +| §1.1 `Blake3` bus, `Multiplicity::Column(MU)`, `TIMESTAMP_0/1` | **no** — confirmed absent: no `Multiplicity`, `TIMESTAMP`, `receive`/`send` anywhere in the file | README finding 1 (the missing input↔output timestamp binding) is invisible to the gate. Confirmed, not re-derived. | +| §4.1 "operands may be linear combos (sum ≤ 255)" | unexercised | `rotr16`/`rotr8` are pure index relabels, so every modelled operand is a single cell. Consistent with the design's actual use. | +| `block_len ∈ [0,64]`, `flags ∈ [0,128)` | modelled as free 32-bit words | model **weaker** — safe, and DESIGN specifies no such constraint either. | + +The BITWISE contracts the gate *assumes* were cross-checked and are real: +`prover/src/tables/bitwise.rs:351-364` enumerates `x,y ∈ [0,256)` and sets +`cols::XOR = x ^ y`; the `ByteAlu` receiver is at `:903-920` and the `AreBytes` +receiver at `:781-796`, both over that domain. DESIGN's cites (`:903`, `:783`) +are accurate. + +--- + +## The assertion tables + +Verdicts: **match** = model = object; **stronger** = model asserts more; +**weaker** = model omits; **not modelled** = outside the model entirely. +`file:line` refers to `blake3-chip/z3_blake_verify.py` unless noted. + +### (a) `blake3_ref.py` → `bref_*` + +Checked mechanically, not by eye: the G schedule and rotation amounts are +**recovered from the oracle by instrumentation** and compared, and every function +is differentially tested. + +| element | oracle | gate | verdict | +|---|---|---|---| +| `IV`, 8 words | `blake3_ref.py:29-32` | `:50-51` | match, element-wise | +| `MSG_PERMUTATION`, 16 indices | `:37` | `:52` | match, element-wise; and is a permutation of 0..15 | +| `MASK32` | `:54` | `:53` | match | +| G body: add order, XOR order, rotation amounts **16,12,8,7 in that order** | `:96-103` | `bref_g` `:72-80` | match — amounts recovered by patching `rotr`/`RotateRight` on both sides | +| G argument order (`v[a]+v[b]+mx` first, `my` second half) | `:96-100` | `:73,77` | match (differential, 300 random + 18 edge inputs) | +| `G_CALLS` 8 quadruples + message indices, **including the 4 diagonals** | `round_fn` `:113-121` | `:58-67` | **match, recovered from `round_fn` by instrumenting `g`** | +| `bref_round` iterates `G_CALLS` in order | `:113-121` | `:83-85` | match | +| `bref_permute` | `permute` `:126` | `:88-89` | match (index-identical) | +| initial `v`: `h[0..8]`, `IV[0..4]`, `t_lo`→v[12], `t_hi`→v[13], `block_len`→v[14], `flags`→v[15] | `compress` `:167-172` | `bref_compress` `:101-104` | match — probed slot by slot at rounds=0, and across 7 counters incl. `2^32−1`, `2^32` | +| counter split `t_lo = t mod 2^32`, `t_hi = t >> 32` | `:164-165` | `:509` (caller) | match | +| permutation applied `r < rounds−1`, i.e. **rounds−1 times** | `:181-182` | `:106-109` | match — counted by instrumentation for rounds 0..8 on both sides: `0,0,1,2,3,4,5,6,7` | +| feed-forward `out[i]=v[i]^v[i+8]`, `out[i+8]=v[i+8]^h[i]` with `h` the **original** CV | `:186-188` | `:110-114` | match | +| rounds parameterisation (6 vs 7 is the loop bound only) | `:176-182` | `:106-109` | match — differential over rounds {0,1,2,5,6,7,8} × 25 vectors | + +No discrepancy. Tampers on `IV`, `G_CALLS` and `MSG_PERMUTATION` are all +detected by the differentials. + +### (b) `DESIGN.md` → `build_g` / `build_round` / `build_compress` + +| DESIGN element | design | model | verdict | +|---|---|---|---| +| §4.1 XOR = 4 per-byte `ByteAlu[XOR]` sends, output pinned + operands range-checked | §4.1 | `xor` `:161-166`, 4 equalities | match (contract verified against `bitwise.rs:351-364`) | +| §4.2 `rotr16` = byte relabel `[b2,b3,b0,b1]`, free | §4.2/§7.6 | `:168-170` | **match — an actual index permutation of the source XOR's byte objects**, not a BV rotate; commits 0 columns, emits 0 constraints; and value-equal to `RotateRight(...,16)` over all 2³² | +| §4.2 `rotr8` = `[b1,b2,b3,b0]`, free | §4.2/§7.6 | `:172-174` | match, same evidence | +| §4.2 `rotr12 = rotl16∘rotl4` (r=4), `rotr7 = rotl16∘rotl9` (r=9) | §4.2 | `:207` `{12:4, 7:9}` | match | +| §4.2 two shift identities `hw·2^r = SLLC·2^16 + SLL` | §4.2 | `:222-223` | match | +| §4.2 recombine `Ylo = SLL_hi + SLLC_lo`, `Yhi = SLL_lo + SLLC_hi` | §4.2 | `:226-227` | match | +| §4.2 `SLL_*`, `SLLC_*` are 16-bit (2 bytes each) | §4.2 | `fresh_word()[:2]` `:213-216` | match | +| §4.2 4 `AreBytes` sends/rotation | §4.2 | **not modelled** | **stronger** (F1); and 3 of the 4 are not load-bearing (F2) | +| §4.3 2-op add: `a+b = s + 2^32·carry`, carry boolean | §4.3 | `add2` `:176-184` | match — the design's *derived* carry and the model's *committed* boolean carry proved equivalent over F_p | +| §4.4 3-op add: `a+b+m = s + 2^32·(c1+c2)`, `c1,c2` boolean | §4.4 | `add3` `:186-197` | match | +| §4.3/§4.4/§5 `s` byte-range-checked free by the next XOR | §4.3/§4.4/§5/§7.4 | `s = self.fresh_word()` (declared bytes) | **stronger** (F1) | +| §4.6 feed-forward, 16 XORs, `out[i+8] = v[i+8] ⊕ h[i]` | §4.6 | `:274-277` | match | +| §4.7 `m` needs explicit `AreBytes`; `h`,`t`,`bl`,`fl` are free | §4.7 | **not modelled** | **stronger** (F1). The *dataflow* half of the claim is verified here mechanically: `h`,`t_lo`,`t_hi`,`bl`,`fl` each feed an XOR, `m` does not | +| §5 every add/shift output feeds a downstream XOR | §5/§7.4 | **not modelled** | **stronger** (F1) — premise verified true here for ROUNDS ∈ {1,2,6,7}: 288 add/shift outputs, 0 unchecked | +| §3 per-G budget 56 byte-cells + 6 carry bits | §3 | counted from the model | match (56 / 6) | +| §2 per-G op mix: 2 add3, 2 add2, 4 xor, 2 shift-rotations, 1 rotr16, 1 rotr8 | §2/§5 | counted from the model | match | +| §7.7 `permute^r` wired from the original `M` columns | §7.7 | `:266-272` | match — 56 index pairs vs the oracle's composition | +| §7.8 IV inlined as constants at `v[8..12]` | §1.1/§7.8 | `const_word` `:258-261` | match | +| §7.9 all field expressions `< 2^35 ≪ p` | §7.9 | `WIDE = 48` | match — the ℤ identity and the mod-p identity proved equivalent under the byte bounds | +| §4.5 μ-gating, all-zero padding | §4.5/§7.1 | **not modelled** | not modelled (F4) | +| §4.8 degree ≤ 3 | §4.8 | **not modelled** | not modelled (F4) | +| §1.1 `Blake3` bus, μ multiplicity, timestamps | §1.1/§3 | **not modelled** | not modelled (F4; README finding 1) | + +### Gate hygiene + +| check | result | +|---|---| +| the G circuit's constraints are satisfiable on their own — MAIN 0's UNSAT is not vacuous | pass | +| the 6-round circuit's constraints are satisfiable on their own | pass | +| constraints emitted per op: xor 4, add2 2, add3 3, rotr12 4, rotr16 0 | pass | +| all 10 canonical 6-round fixtures reproduce from the live oracle (the positive controls are not anchored to a stale file) | pass | +| …and none of them equals the 7-round compression of the same input | pass | +| `gen_7round_vector` returns the oracle's own 7-round output | pass | +| the assumed `ByteAlu[XOR]` / `AreBytes` contracts match `prover/src/tables/bitwise.rs` | pass | +| `check_g()` UNSAT, `check_compress(0)` UNSAT, `check_g(swap_g_operand)` SAT (`--slow`) | pass | + +--- + +## Documentation defects (F5) + +Not soundness; a reader following the citations is misled. + +* **§7.3's mechanism is backwards.** "a wrong `SLL` admits a large field `SLLC`" + — the forgery keeps `SLLC` small (`2^r`, inside its own bound) and makes `SLL` + large (`p − 2^r`). Witness above. +* **§4.8's recombine row over-states its degree.** `μ·(Ylo − SLL_hi − SLLC_lo)` + is linear in committed columns → body degree 1, ×μ = 2. The table says 2 → 3. + Safe-side wrong; the "no constraint exceeds 3" verdict is unaffected. +* **§3 and §4.3 disagree on whether `add2` commits a carry column.** §3's per-G + table counts 1 carry bit per `add2` (6 per G); §4.3 makes it a *derived linear + expression* `(a+b−s)·INV_SHIFT_32` with no column. Semantically equivalent + (proved), but 96 cells per compression hang on the reading, in a design whose + headline is a cell count. +* The gate's docstring "Every committed column … is a FREE bitvector" should say + "a free **byte**" — the distinction is F1. + +--- + +## Could not determine + +Stated so the boundary is explicit rather than implied. + +1. **Anything about a Rust chip.** There is none. README finding 2 (48 G + instances emitted separately; a wrong column index in instance #37) is + unauditable until it exists, and F1's forgeries are all statements about what + a future implementation must not do. +2. **The bus layer.** Not modelled by the gate, not audited here. F1's message + and `h` constructions become live or benign depending on it; README finding 1 + (the missing input↔output timestamp binding) sits in the same place. + Confirmed absent from the gate, per the brief — not re-derived. +3. **The `--full` monolithic UNSATs** (`check_round`, `check_compress(2/6/7)`) + were **not run** — 30-40 min timeouts each. I audited the model they run on, + not their verdicts. Note that they inherit F1 in full: a monolithic 6-round + UNSAT is still an UNSAT about a model in which every cell is a declared byte. +4. **The `HWSL` inline soundness proof** the design defers to + (`../keccak-verify/hwsl_inline_test.py` Part 2) — that directory is not in + this artifact. §C's composed field model re-derives the shift-identity result + independently, so the conclusion does not rest on the missing file, but the + cited proof was not read. +5. **Completeness.** Every result here is about soundness (can a wrong witness + pass). Whether an honest trace generator can *produce* the witnesses — the + `AreBytes` send layout, the carry values — is unchecked; a mismatch there is + an unprovable honest witness, not a forgery. +6. **Whether the recovered artifact is byte-identical to the 2026-07-23 + original** (README's own open item). Unchanged by this audit. + +--- + +## Regression suite + +`audit_gate_transcription.py`, 73 checks (76 with `--slow`), all passing, every +one paired with a tamper that must break it: + +``` +A reference transcription (a): constants element-wise; G_CALLS and rotation + amounts RECOVERED from the oracle by instrumentation; differential g / + round / permute / compress over rounds {0,1,2,5,6,7,8}; counter split + across 2^32; permute-application count 0,0,1,2,3,4,5,6,7; v-layout probed + slot by slot. Tampers: IV, G_CALLS, MSG_PERMUTATION — all detected. +B circuit transcription (b): rotr16/rotr8 are index relabels by object + identity, commit no columns, value-equal to RotateRight; per-G cell and op + census; SSA range-check provenance over one G and over ROUNDS 1/2/6/7; + h[0..4]'s single feed-forward consumer; message indexing under permute^r, + 56 pairs; what the model does not represent (no range object, no mu, no + bus). Tampers: a wrong relabel, a G whose add output loses its XOR + consumer, a swapped MSG_PERMUTATION entry, a swapped G_CALLS message pair + — all detected. +C the dangerous direction, in the field: add2/add3 pinned with the range + check and FORGEABLE without it (concrete witness a=b=0x80000000 -> s=2^32); + the rotation output needs no range check of its own; the 32-configuration + bound lattice with non-vacuity; the composed forgery at X=0xFFFFFFFF, + enumerated exhaustively; both width-audit positives re-derived symbolically + for all inputs; the message-cell collision. +D hygiene: non-vacuity of MAIN 0 and the 6-round model; per-op constraint + counts; derived-vs-committed carry equivalence; canonical fixtures + reproduce from the live oracle; Z-vs-F_p equivalence of the WIDE=48 model; + the BITWISE contracts checked against prover/src/tables/bitwise.rs; + (--slow) the gate's own BV verdicts. +``` diff --git a/thoughts/blake3/README.md b/thoughts/blake3/README.md new file mode 100644 index 000000000..1892bbf47 --- /dev/null +++ b/thoughts/blake3/README.md @@ -0,0 +1,215 @@ +# BLAKE3 accelerator — oracle + gate-proved chip design (RECOVERED, re-validated) + +**Provenance: recovered 2026-07-29 from subagent transcripts, not from a +backup.** The original work (2026-07-23) was written to a session scratchpad +under `/private/tmp/...`, never committed, and the scratchpad was gone by the +time anyone looked. The files here were reconstructed by replaying the `Write` +and `Edit` tool calls out of +`.claude/projects/.../1c23da47-.../subagents/agent-ablake3-{oracle,chip-design}-*.jsonl` +(5 Writes + 11 Edits, every Edit applied cleanly — no partial replays). + +Committing them is the point: this is the second campaign whose verification +artifacts were nearly lost to a scratchpad. Anything worth keeping belongs in +the repo. + +## What this is + +A BLAKE3 compression-function accelerator taken to a **gate-proved design**, +and — as of 2026-08-05 — **implemented in Rust** (PR #903: executor syscall +`u64::MAX-2`, chip `prover/src/tables/blake3.rs`, adversarially reviewed, e2e +prove+verify green, measured 12.2× keccak merges/s). The named "6-round +collision resistance" assumption (A6R) is recorded in the spec +(`spec/blake3.typ`) and in `blake3-chip/IMPLEMENTATION.md`; production use as +a Merkle/FS hash still requires ratifying it (or shipping the assumption-free +7-round instantiation, which costs ~10-12% more per merge). + +Purpose is **internal** (Merkle / Fiat–Shamir replacement candidate; the 6-round +variant is the primary target, K12 as precedent). The EVM has no BLAKE3 — only +the BLAKE2b-F precompile at 0x09 (EIP-152), variable-round and rarely used, so +that stays guest code. + +## Contents + +| file | what it is | +|---|---| +| `blake3-oracle/blake3_ref.py` | independent reference implementation | +| `blake3-oracle/test_oracle.py` | three-anchor validation harness; emits the canonical 6-round vectors | +| `blake3-oracle/ORACLE.md` | anchor results and the contract map | +| `blake3-oracle/official_test_vectors.json` | 35-case vector set — **see provenance note below** | +| `blake3-oracle/canonical_6round_vectors.json` | 10 pinned 6-round vectors, regenerated by the harness | +| `blake3-chip/DESIGN.md` | chip design + §7 risk ledger | +| `blake3-chip/IMPLEMENTATION.md` | Rust-implementation notes: deltas from the design + gates run | +| `blake3-chip/z3_blake_verify.py` | the soundness gate | +| `TRANSCRIPTION-AUDIT.md` | audit: oracle → gate transcription | +| `GATE-TRANSCRIPTION-AUDIT.md` | audit: gate constraint-model transcription | +| `audit_gate_transcription.py` | the executable half of the gate audit | +| `poseidon2-cost-study.md` | poseidon2-vs-blake3 cost study (2026-08-05) | +| `ground-truth/` | tiny Rust generator that produced the vector set from the official `blake3` crate | + +## Re-validation, 2026-07-29 — everything runs and passes + +Both fixtures were missing from the recovery (they had been downloaded or +generated, so no tool call held them). Both are now restored, and **every claim +in `DESIGN.md` §9 reproduces**: + +``` +oracle: [1] official vector set PASS 35/35 x 3 modes + [2] blake3 PyPI package SKIP (not installed) + [3] Plonky3 blake3-air PASS 20,000 compressions +gate: G-function UNSAT (covers all G) : True + init+feed-forward UNSAT (rounds=0): True + negative controls all SAT : True (5/5) + positive controls all SAT : True (6-round seeds 0,1,2 + 7-round) + width audit (bound necessity) : True + OVERALL: PASS +``` + +Independently of the harness, `blake3_ref.py` reproduces the published +known-answer vectors exactly: `blake3("")` = `af1349b9f5f9…41f3262` and +`blake3("abc")` = `6437b3ac38…d5bd9d85`. That single check exercises the IV, the +G function, all four rotations, the permutation *and its count*, the +feed-forward, the flag bit values and little-endian packing at once. + +Two independent reviews (different models, no coordination) found **no +discrepancy in the primitive**. One wrote a from-scratch BLAKE3 structured +deliberately differently and differentially tested 100k random compressions, all +128 flag values × {6,7} rounds, a rounds sweep 0..8, and whole-hash over 227 +lengths × 4 modes — zero mismatches — and confirmed `r < rounds−1`, i.e. **6 +permutes for 7 rounds**, so the classic off-by-one is absent. + +### ⚠ Provenance of `official_test_vectors.json` + +It was **regenerated from the official `blake3` Rust crate v1.8.5** +(`ground-truth/`), not downloaded from the upstream repo. It carries the +official parameters — key `whats the Elvish word for friend`, context +`BLAKE3 2019-12-27 16:29:52 test vectors context`, the same 35 input lengths — +and case 0 matches the independently-known published digest. + +This is a **genuine, non-circular anchor**: the Rust crate is the BLAKE3 +authors' reference implementation and is entirely independent of +`blake3_ref.py`. But it is *not* the published artifact, and `test_oracle.py` +still labels it "Official test_vectors.json". Read it as "checked against the +official reference implementation using the official vector parameters". + +### What the two reviews pinned that no anchor covers + +- **Counter split order at `t ≥ 2^32` — confirmed.** `t_lo = t mod 2^32 → v[12]`, + `t_hi = t >> 32 → v[13]`. Verified *behaviourally* against the official crate + through two independent counter paths (`OutputReader::set_position` and + `hazmat::HasherExt::set_input_offset`), over counters 0 … 2^47 including + 2^32−1, 2^32, 2^32+1: **44/44**. Negative control — swapping the halves — + breaks 5 of 6 chunk cases, the sixth being `counter = 0`, correctly invariant. + This closes ORACLE.md's own open question O5. +- **Message schedule count *and direction*.** Iterating `permute` from the + identity reproduces **all seven rows** of the crate's precomputed + `MSG_SCHEDULE`. Three mutants (permute before round 0, skip the 0→1 permute, + inverse direction) are all caught. A fourth — permuting *after* the last + round — is provably a no-op, so the trailing-permute guard is an optimisation + and cannot hide an off-by-one either way. +- `compress` does not mutate its arguments; incremental `update()` equals the + one-shot path over 60 random split patterns. + +### Harness defects — FIXED + +1. ~~`main()` printed `VALIDATION STATUS: VALIDATED … anchored on official test + vectors + official PyPI package + Plonky3` **even when anchor 2 SKIPped**~~ — + the `status` dict was written and never read. **Fixed:** the banner now + reports what actually ran (`VALIDATED` / `PARTIALLY VALIDATED` / `NOT + VALIDATED`) and names the anchors it is *not* anchored on. Verified by + running with a fixture removed. +2. ~~The missing-file failure **cascaded**~~ — one `FileNotFoundError` killed + anchors 2 and 3 *and* the canonical-vector emitter, which is why the gate's + positive controls were blocked on an unrelated download. **Fixed:** anchors + are independent; a missing fixture SKIPs only itself. Verified — with + `official_test_vectors.json` removed, anchor 3 still runs and the vectors are + still emitted. +3. ~~Anchor 1 was labelled "Official test_vectors.json"~~ — it is regenerated + from the crate. **Fixed:** relabelled "Official-parameter vectors" and the + run prints its provenance. + +### Known harness defects — still open (low severity) + +4. `test_internal_consistency` carries a comment describing a feed-forward + recomputation (*"recompute v to check"*) that **is not implemented** — it only + checks output length and the CV prefix. +5. **`test_6round_derivation`'s first assertion is a tautology** — + `compress_6round`'s body *is* `compress(rounds=6)`. ORACLE.md §2.6 calls it + the "Code-diff anchor"; it establishes nothing. The differs-from-7r half is + real. +6. **Footgun for the Rust phase:** `compress(...)` defaults to `rounds=7`, so a + 6-round caller that omits the kwarg silently gets 7. Trace generators must + call `compress_6round`. Left as-is deliberately: changing the validated + oracle's signature would invalidate the anchors it just passed. +7. ORACLE.md §5's closing ratio is internally inconsistent: ~5–6k cell-equiv + against 24×1480 = 35,520 is ≈1/6, not the "¼–⅓" its prose claims. Superseded + by DESIGN.md §6's ≈1/15 against a 77,000 baseline — which is the number that + was actually derived. + +## DESIGN findings (review, 2026-07-29) — FIXED IN THE DESIGN + +1. **The internal `Blake3` bus had no input↔output binding.** §1.1 defined a + receive of `(h, m, t, block_len, flags)` and a separate send of `out[0..16]`, + both at multiplicity μ, while §3 listed `TIMESTAMP_0/1` as "bus binding + (internal variant **may omit**)". Omit it and, with two compressions in a + trace, row A can receive inputs_A and send out_B while row B does the + reverse: every tuple appears once on each side, **the bus balances**, and + both callers read a wrong result. The design's own cited precedent does not + do this — keccak carries `TIMESTAMP_0, TIMESTAMP_1` in *both* halves of its + internal bus (send at round 0, receive at round 24). + **Fixed:** §1.1 now states the binding is mandatory in both tuples, with the + attack and the keccak precedent spelled out; §3's "may omit" is gone; and it + is item 10 of §7's soundness-critical list. Also recorded there: **the gate + cannot catch a violation**, since it models arithmetic with no bus layer. +2. **"Covers every G, hence every round" is a model argument.** MAIN 0 proves + one G under free inputs; in Rust the 48 instances are emitted separately, so + a wrong column index in instance #37 is invisible to it. + **Fixed:** now item 11 of §7, pointing at the concrete positive controls as + the thing that covers it and requiring `--full`'s monolithic UNSAT before + Rust ships. The controls themselves were unrunnable at review time and now + run and pass 4/4, so the residual risk is materially lower than when the + finding was written. +3. **The 3-op add carry encoding is ambiguous** — `(c1,c2) = (1,0)` and `(0,1)` + both encode carry 1. Checked: it does not admit a wrong `s`, so this is a + note rather than a defect, recorded so nobody "fixes" it into a bug. **No + change made, deliberately.** + +## Still unaudited — where to send the next reviewer + +*(2026-08-05 update: the transcription audit this section asks for has since +been done — see `TRANSCRIPTION-AUDIT.md` and `GATE-TRANSCRIPTION-AUDIT.md`, +which found and fixed the issues recorded in DESIGN.md §4.2/§7. The section is +kept for its account of WHY that audit mattered.)* + +Two independent reviews established that **the oracle defines the right +function**, so the gate's UNSATs are about the right function. They did *not* +audit the step after that: **nobody has checked the z3 gate's transcription of +the oracle into constraints.** Only its constants block +(`z3_blake_verify.py:50-80`) was spot-checked, and it matches exactly. + +That is the highest-value next pass, and the EC campaign is the reason to take +it seriously: the equivalent audit there +(`thoughts/ec-recover-opt/gate/TRANSCRIPTION-AUDIT.md`) found three premises the +gate asserted about the chip and never read, one of them hiding a working +forgery. The dangerous direction is a model **stronger** than the thing it +models — it yields UNSAT where the real object is forgeable, and a positive +anchor cannot catch it, because honest inputs satisfy a correct model and an +over-strong one equally well. + +Also still thin: neither review verified the recovery is *byte-identical to the +original* — only that the artifact is correct BLAKE3, which is a different and +weaker claim; and the historical counts ("35/35×3", "92/92" against PyPI +v1.0.9) remain unreproduced as recorded. + +## If this is picked up again + +*(2026-08-05: it was picked up — see the top of this file. The A6R assumption +is now written down in `spec/blake3.typ`; formal ratification remains open.)* + +The blocking item is a **protocol decision, not an engineering one**: the gate +proves the chip matches the reference, *not* that 6 rounds are secure. That +needs a named, signed assumption in the spec. + +Note for anyone citing precedent: the EC `lincomb2` design study justified its +NUMS assumption with "like blake3's 6-round assumption" — but no such assumption +was ever recorded in the spec, because this work never shipped. It was a +precedent for something that had not happened. diff --git a/thoughts/blake3/TRANSCRIPTION-AUDIT.md b/thoughts/blake3/TRANSCRIPTION-AUDIT.md new file mode 100644 index 000000000..e96cc8b67 --- /dev/null +++ b/thoughts/blake3/TRANSCRIPTION-AUDIT.md @@ -0,0 +1,225 @@ +# Transcription audit — does the BLAKE3 gate assert what the design and oracle say? + +Auditor: independent pass, 2026-07-29, branch `spike/blake3-recovered`. +Objects audited: + +- **oracle**: `blake3-oracle/blake3_ref.py` + `test_oracle.py` (does it define the right function?) +- **gate**: `blake3-chip/z3_blake_verify.py` against `blake3-chip/DESIGN.md` and the oracle + (is the constraint transcription faithful? can the model be stronger than the chip?) +- **the uncommitted fixes** on `DESIGN.md` / `test_oracle.py` (the 3 design findings + + harness defects #1/#3) — verified, see §5. + +Method mirrors `../ec-recover-opt/gate/TRANSCRIPTION-AUDIT.md`: only one direction is +dangerous. A model **weaker** than the chip yields spurious SAT (false alarm); a model +**stronger** than the chip yields UNSAT on a forgeable chip (false assurance), and no +positive anchor can see that, because an honest witness satisfies a correct model and an +over-strong one equally well. Here there is no Rust chip yet — the gate is the only +executable statement of the design — so the audit is gate ↔ design + oracle, and every +place the gate *cannot see* is a place the future Rust must get right by construction. + +Reproduce: everything below ran with `blake3/venv` (z3 5.0.0, blake3 PyPI 1.x), +`ground-truth` (official `blake3` crate v1.8.5, pure-Rust), and the vendored +`others/Plonky3/blake3-air`. Mutant/tamper scripts were scratch files, not committed. + +--- + +## Verdict + +**No over-strong or mis-transcribed premise found.** Every equation in the gate matches +the design it encodes (§2 table), the gate's reference is behaviourally identical to the +externally-anchored oracle (§3), the gate is *sensitive* to every wiring-bug class we +could construct — including classes with no shipped negative control (§4, 7/7 mutants +fire) — and the width analysis holds with slack (expressions ≤ ~2^41 vs the 2^48 model +width). The two re-runs reproduce the recorded board: default run **OVERALL: PASS**; +`--full` monolithic UNSATs: **SEE §6**. + +The honest map of what a green board does NOT cover (§2, "no automated check" rows) +is where the remaining risk lives: μ-gating/padding, input range checks, the degree-3 +ledger, the bus layer, and the precomputed-table contracts. All are documented in +DESIGN §7; the uncommitted fixes added items 10–11. None is new. + +## §1 — Oracle re-validation (does the oracle define the right function?) + +Re-ran and independently re-derived, all green: + +| check | result | +|---|---| +| harness `test_oracle.py`, anchor 1 (official-parameter vectors) | PASS 35/35 × 3 modes | +| anchor 2 (official `blake3` PyPI pkg) — **live this time** (was SKIP) | PASS 92/92 | +| anchor 3 (Plonky3 `blake3-air` port, direct compression) | PASS 20 000/20 000 | +| banner honesty (defect #1 fix) | reads VALIDATED only because all three ran (see §5) | +| known-answer: `blake3("")`, `blake3("abc")` | exact match to published digests | +| differential vs PyPI: 140 lengths × {default, keyed, derive} + 48 XOF-length checks | 468/468 | +| counter split `t_lo/t_hi` vs official crate, XOF `set_position` path, t ∈ {0,1,2, 2^32−2, 2^32−1, **2^32**, **2^32+1**, 2^40, 2^47} | **9/9** (scratch `counter_probe.rs` + `blake3_ref.compress`) | +| swapped-halves negative control | breaks 7/9; the 2 invariants are t=0 and t=0x1_0000_0001 (t_lo==t_hi), both correctly invariant | +| message schedule count+direction: `permute^r` from identity vs the crate's precomputed `MSG_SCHEDULE` | all 7 rows exact | + +The historical counts ("35/35×3", "92/92") now reproduce as recorded. ORACLE.md O5 +(counter width) remains closed — re-confirmed against the crate at t ≥ 2^32. + +## §2 — Per-premise transcription table (gate ↔ DESIGN ↔ oracle) + +| gate premise | source | verified | how | +|---|---|---|---| +| `IV`, `MSG_PERMUTATION` constants | DESIGN §1, oracle §2.1, Plonky3 `constants.rs` | ✅ exact | 3-way diff | +| `G_CALLS` (8 index tuples + msg order) | oracle `round_fn` | ✅ exact | diff | +| `bref_*` reference independent of circuit wiring | DESIGN §8 | ✅ | 200 concrete trials vs `blake3_ref.compress` (rounds 6+7), 0 mismatch; leading-permute mutant differs ⇒ `r < rounds−1` guard direction correct | +| init layout `v = h ‖ IV[0..4] ‖ t_lo,t_hi,bl,fl` | oracle §2.4, DESIGN §7.8 | ✅ | MAIN 1 UNSAT + `wrong_iv` control | +| feed-forward `out[i]=v[i]⊕v[i+8]`, `out[i+8]=v[i+8]⊕h[i]` | oracle §2.4, DESIGN §4.6 | ✅ | MAIN 1 UNSAT + `drop_ff_xor` control | +| schedule = `permute^r` of the ORIGINAL `M` | DESIGN §7.7 | ✅ | `wrong_msg_index` control + `permute_inverse` mutant + positive controls | +| `add2`: `a+b = s + 2^32·c`, c boolean | DESIGN §4.3 | ✅ | equation exact; field-level necessity of booleanity confirmed (this audit, §4) | +| `add3`: `a+b+m = s + 2^32·(c1+c2)`, c1,c2 boolean | DESIGN §4.4 (O1 option c) | ✅ | equation exact; width audit drop→SAT | +| `rotr16=[b2,b3,b0,b1]`, `rotr8=[b1,b2,b3,b0]` free relabels | DESIGN §4.2/§7.6 | ✅ | relabel mutants flip check to SAT (§4) | +| `rotr12/rotr7` shift identity `hw·2^r = SLLC·2^16 + SLL`, r=4/9 | DESIGN §4.2 | ✅ | equation exact; `rot_wrong_amount` control | +| recombine `Ylo=SLL_hi+SLLC_lo`, `Yhi=SLL_lo+SLLC_hi` | DESIGN §4.2 | ✅ | recombine mutants flip to SAT (§4) | +| ByteAlu[XOR] / AreBytes table contracts | `prover/src/tables/bitwise.rs` | ⚠ assume-guarantee | documented; same assumption keccak gate makes; **no automated check here** | +| μ-gating / all-zero padding (μ=1 modelled) | DESIGN §4.5, §7.1 | ⚠ gate cannot see | no bus/multiplicity layer; on the implementer | +| input range checks (h,t,bl,fl free via XOR; **m needs explicit AreBytes**) | DESIGN §4.7, §7.5 | ⚠ gate cannot see | gate inputs are bytes by construction; a dropped `AreBytes(m)` in Rust is invisible here | +| degree ≤ 3 ledger | DESIGN §4.8 | ⚠ no automated check | manual ledger; gate models equations, not degrees | +| `Blake3` bus TIMESTAMP binding (findings fix) | DESIGN §1.1/§7.10 | ⚠ gate cannot see | no bus layer; verified by construction, see §5 | +| 48 G instances wired as MAIN 0 models | DESIGN §7.11 | ✅ concrete | positive controls run all 48; `--full` monolithic UNSATs (§6); per-instance index mutant fires (§4) | +| WIDE=48 model cannot wrap | gate internals | ✅ | worst expression ≈ 2^41 (add3 with 8-bit carries) ≪ 2^48 | + +## §3 — Reference (`bref_*`) independence + +The gate's soundness rests on `bref_*` being an independent statement of BLAKE3. It is +structurally independent (32-bit BV `RotateRight`/`+`/`^` vs the byte-level circuit) and +behaviourally identical to the oracle: 200 random concrete inputs, rounds ∈ {6,7}, +0 mismatches. The permute guard `r < rounds−1` matches the oracle's (a leading-extra-permute +variant provably differs). The one structural mirror both share with the oracle — the +constants and `G_CALLS` table — is pinned by the *external* anchors (crate, PyPI, Plonky3), +so a common-mode bug there would have to be a bug in BLAKE3 itself. + +## §4 — Gate sensitivity: shipped controls + mutation sweep + +Shipped controls all reproduced (default run): 5/5 structural SAT, width audit 4/4, +positive controls 4/4 SAT. + +Mutation sweep (scratch, not committed) — bug classes with **no shipped negative control**; +each was injected into a copy of the circuit builders and must flip its check to SAT: + +| mutant | class | result | +|---|---|---| +| `rotr16_bad_relabel` | free-rotation byte order (DESIGN §7.6) | **sat — detected** | +| `rotr8_bad_relabel` | free-rotation byte order | **sat — detected** | +| `rotr12_bad_recombine` | carry paired to wrong halfword | **sat — detected** | +| `swap_mx_my` | message operand order in G | **sat — detected** | +| `permute_inverse` | schedule direction | **sat — detected** | +| `bad_diag_index` | one wrong column in G instance #7 (per-instance wiring, §7.11) | **sat — detected** | +| `rounds_off_by_one` | round-loop bound | **sat — detected** | + +Field-level addition: the shipped width audit demonstrates bound-necessity only for the +**3-op** add. This audit verified the same for the **2-op** add: booleanity present → +UNSAT (pinned), dropped → SAT (forgeable mod p). Same class, now demonstrated for both. + +## §5 — Verdict on the uncommitted fixes + +- **Finding 1 (bus input↔output binding) — FIX REAL.** `DESIGN.md` §1.1/§3/§7.10 now + mandate `TIMESTAMP_0/1` in both `Blake3` receive and send. The cited precedent checks + out: `prover/src/tables/keccak.rs:264-319` sends `(ts, 0, input_state)` and receives + `(ts, 24, output_state)` on the internal `Keccak` bus with `TIMESTAMP_0/1` in *both* + tuples (`BusValue::Packed` at `cols::TIMESTAMP_0/1`). The swap-attack reasoning is + sound: with no common key, rows A/B exchanging output tuples keeps every tuple + appearing once per side, so LogUp balances while both callers read wrong results. + Correctly documented as gate-invisible (no bus layer). +- **Finding 2 ("covers every G" is a model argument) — FIX REAL.** §7.11 records it; + the positive controls do run the full 48-instance pipeline concretely, and this audit's + per-instance index mutant backs it. (Superseded in part: this bullet originally also + cited the `--full` monolithic UNSATs as backing. They were run on 2026-08-06 and came + back `unknown` on all four queries — see §6 — so they support nothing either way. The + argument rests on the concrete positive controls and the index mutant.) +- **Finding 3 (carry encoding ambiguity `(1,0)`/`(0,1)`) — correctly classified + harmless.** The sum identity constrains only `c1+c2`; `s` is pinned regardless. +- **Harness defect #1 (banner overstatement) — FIX REAL.** The banner now reads from + the status dict; exercised live (below). +- **Harness defect #3 (missing-fixture cascade) — FIX REAL.** With + `official_test_vectors.json` renamed away: anchor 1 SKIPs alone, anchors 2/3 PASS, + the canonical-vector emitter still runs (it is now unconditional), banner reads + "PARTIALLY VALIDATED … NOT anchored on: official-parameter vectors". Fixture restored + afterwards; regenerated `canonical_6round_vectors.json` is byte-identical. + +## §6 — Gate re-runs + +- default (`z3_blake_verify.py`), z3 5.0.0: **OVERALL: PASS** (board identical to §9 of + DESIGN.md). +- `--full` (monolithic symbolic round / rounds=2 / 6-round / 7-round UNSATs), run + 2026-08-06: **ATTEMPTED-INCONCLUSIVE — no pass, and no counterexample.** The run took + ~145 min and exited 1 (`OVERALL: FAIL`), but all four monolithic queries returned + `unknown`, not `sat`: + + ``` + round (clean) -> unknown (want unsat) + compress rounds=2 -> unknown (want unsat) + compress rounds=6 -> unknown (want unsat) + compress rounds=7 -> unknown (want unsat) + ``` + + `unknown` is z3's resource-limit return (`s.set("timeout", timeout_ms)` then + `s.check()`, `z3_blake_verify.py:320-321`/`:340-341`); the verdict tests `== unsat` + (line 553), so a timeout is scored `False` and pulls OVERALL to FAIL. The four + budgets sum to 140 min against ~145 min wall, i.e. every check burned its full + allowance. **Nothing was disproven; nothing was proven monolithically.** The fast + board is unchanged and green: + + ``` + G-function UNSAT (covers all G) : True + init+feed-forward UNSAT (rounds=0): True + negative controls all SAT : True + positive controls all SAT : True (full 6-/7-round pipeline, concrete) + ``` + + Consequence for §5's Finding 2 above: the "`--full` monolithic UNSATs (§6)" cited + there as backing the per-instance coverage argument did **not** land, so that + argument currently rests on the concrete positive controls and the per-instance + index mutant alone. Remediation: rerun with a much larger timeout budget on a + server (single-threaded, CPU-bound), and/or restructure the monolithic query as + round-by-round induction. + +## §6b — Reconciliation with the second, independent audit (`audit_gate_transcription.py`) + +A separately-authored executable audit (74/74 checks pass, run this session) agrees with +every verdict above and sharpens three points this audit stated more coarsely: + +1. **Rotation bound necessity, refined.** The load-bearing bound set is *at least one of* + `{SLL_lo, SLL_hi}` — every configuration with neither is forgeable, every one with + either is pinned; the `SLLC` bounds are not load-bearing. DESIGN §4.2's "the tight + SLL bound" should read "a tight bound on at least one SLL halfword". The composed + (whole-rotation) forgery with both SLL bounds dropped exists for exactly **one** + input, `X=0xFFFFFFFF` (forged `Y=0`), not for arbitrary inputs. +2. **Doc note (safe direction):** DESIGN §4.8's degree-ledger row for the recombine + identity overstates (claims body 2 → 3 after ×μ; the body is linear, so 1 → 2). + The "no constraint exceeds 3" verdict is unaffected. +3. **Doc/cost inconsistency:** DESIGN §3's per-G table commits 1 carry *column* per add2, + while §4.3 makes that carry a *derived* linear expression (`(a+b−s)·INV_SHIFT_32`). + The two are equivalent over F_p (proven by that suite) but differ by 96 cells per + compression in the §6 cost table. The gate models the committed form. + +It also independently confirms this report's two "gate cannot see" rows with explicit +forgeries: the missing `AreBytes(m)` (§2, DESIGN §4.7) and the declared-not-derived +input range checks. + +## §7 — Still open (pre-existing, report-only per audit scope) + +1. Harness defect #2: `test_internal_consistency`'s comment promises a feed-forward + recomputation that is not implemented (`test_oracle.py:227`). Comment lies; check is + shallow (length + CV prefix only). +2. Harness defect #4: `test_6round_derivation`'s first assertion is a tautology + (`compress_6round` *is* `compress(rounds=6)`). The differs-from-7r half is the real + content. +3. Harness defect #5 (footgun for the Rust phase): `compress(...)` defaults `rounds=7`. + Trace generators must call `compress_6round` / pass `rounds=` explicitly. +4. ORACLE.md §5 prose "¼–⅓ of a keccak permutation" is inconsistent with its own + ~5–6k figure (≈1/6 of 24×1480); superseded by DESIGN §6's derived ≈1/15. Doc-only. +5. `ground-truth/Cargo.toml` could not build inside this repo ("believes it's in a + workspace"). Fixed with an empty `[workspace]` table — **this audit touched that one + committed file**; without it the documented regeneration flow fails out of the box. +6. Suggestion (not a defect): fold the §4 mutant sweep and the add2 field check into the + shipped gate as regression controls, so future edits to the gate are held to the same + sensitivity. + +## §8 — Scratch artifacts left in the tree (untracked; commit or delete, user's call) + +- `thoughts/blake3/venv/` (z3 5.0.0 + official `blake3` PyPI pkg) +- `thoughts/blake3/ground-truth/src/bin/counter_probe.rs` (the t≥2^32 counter probe) +- `thoughts/blake3/ground-truth/target/`, `thoughts/blake3/blake3-oracle/__pycache__/` +- mutant sweep + add2 field check: `/tmp/blake_mutants.py` and heredocs (not in tree) diff --git a/thoughts/blake3/audit_gate_transcription.py b/thoughts/blake3/audit_gate_transcription.py new file mode 100644 index 000000000..6cd9b1000 --- /dev/null +++ b/thoughts/blake3/audit_gate_transcription.py @@ -0,0 +1,1198 @@ +""" +Transcription audit of `blake3-chip/z3_blake_verify.py` — an EXECUTABLE +regression suite for GATE-TRANSCRIPTION-AUDIT.md. + +Two transcriptions are under test, and only one direction is dangerous. + + (a) blake3-oracle/blake3_ref.py -> the gate's `bref_*` BV reference. + If these diverge, every UNSAT the gate reports proves the chip matches + the WRONG function. + + (b) blake3-chip/DESIGN.md -> the gate's `build_g/build_round/ + build_compress` circuit model. + A model WEAKER than the designed chip yields a spurious SAT — a false + alarm, safe. A model STRONGER yields UNSAT where the real object is + forgeable — false assurance, and no positive control can see it, + because an honest witness satisfies a correct model and an over-strong + model equally well. + +Every check below is paired with a TAMPER that must make it fail; a check +that does not bite is itself reported as a failure. Nothing outside this +file is modified: tampers are applied to in-memory copies and reverted. + +Run: python3 audit_gate_transcription.py (fast sections) + python3 audit_gate_transcription.py --slow (+ the BV UNSATs, ~5 min) +""" +import importlib.util +import itertools +import os +import random +import sys + +from z3 import ( + And, BitVec, BitVecVal, Concat, Int, IntVal, Or, RotateRight, Solver, + is_bv, sat, simplify, unsat, +) + +HERE = os.path.dirname(os.path.abspath(__file__)) +P = 2**64 - 2**32 + 1 # Goldilocks +MASK32 = 0xFFFFFFFF + + +def _load(name, relpath): + spec = importlib.util.spec_from_file_location(name, os.path.join(HERE, relpath)) + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +ORA = _load("blake3_ref", "blake3-oracle/blake3_ref.py") +GATE = _load("z3_blake_verify", "blake3-chip/z3_blake_verify.py") + + +# --------------------------------------------------------------------------- +# result bookkeeping +# --------------------------------------------------------------------------- +RESULTS = [] + + +def record(section, name, ok, detail=""): + RESULTS.append((section, name, bool(ok), detail)) + flag = "PASS" if ok else "**FAIL**" + print(f" [{flag}] {name}" + (f" {detail}" if detail else "")) + return ok + + +def note(text): + """An observation that is reported but is not a pass/fail check.""" + print(f" [note] {text}") + + +def ora_G_CALLS(): + """The G-call schedule RECOVERED from the oracle (blake3_ref.round_fn), + with sentinel message words so the mx/my indices come back too.""" + calls = [] + orig = ORA.g + + def spy(state, a, b, c, d, mx, my): + calls.append((a, b, c, d, mx - 1000, my - 1000)) + ORA.g = spy + try: + ORA.round_fn([0] * 16, [1000 + i for i in range(16)]) + finally: + ORA.g = orig + return calls + + +def bites(section, name, tamper_fn): + """A check must FAIL under its tamper, or the check is decorative.""" + try: + detected = tamper_fn() + except Exception as exc: # a crash is also detection + detected = True + record(section, f"tamper bites: {name}", True, f"(raised {type(exc).__name__})") + return True + return record(section, f"tamper bites: {name}", detected, + "" if detected else "TAMPER NOT DETECTED — the check is vacuous") + + +# =========================================================================== +# SECTION A — transcription (a): blake3_ref.py -> bref_* +# =========================================================================== +def section_A(slow): + print("\n" + "=" * 74) + print("A REFERENCE TRANSCRIPTION — blake3_ref.py -> bref_* (the BV oracle)") + print("=" * 74) + + # -- A1 constant tables, element by element ---------------------------- + record("A", "IV identical (8 words, element-wise)", + list(GATE.IV) == list(ORA.IV), f"{[hex(x) for x in GATE.IV[:2]]}...") + record("A", "MSG_PERMUTATION identical (16 indices, element-wise)", + list(GATE.MSG_PERMUTATION) == list(ORA.MSG_PERMUTATION), + str(GATE.MSG_PERMUTATION)) + record("A", "MSG_PERMUTATION is a permutation of 0..15", + sorted(GATE.MSG_PERMUTATION) == list(range(16))) + record("A", "MASK32 identical", GATE.MASK32 == ORA.MASK32) + + def tamper_iv(): + old = GATE.IV[3] + GATE.IV[3] ^= 1 + bad = list(GATE.IV) != list(ORA.IV) + GATE.IV[3] = old + return bad + bites("A", "IV comparison", tamper_iv) + + # -- A2 the G-call schedule, RECOVERED from the oracle ------------------ + ora_calls = ora_G_CALLS() + record("A", "G_CALLS == the oracle's round_fn call sequence (recovered)", + [tuple(x) for x in GATE.G_CALLS] == ora_calls, + f"{len(ora_calls)} calls") + record("A", "every G quadruple has 4 DISTINCT state indices " + "(so check_g's a,b,c,d=0,1,2,3 instance is general)", + all(len({a, b, c, d}) == 4 for a, b, c, d, _, _ in GATE.G_CALLS)) + record("A", "the 8 G-calls touch each of the 16 state slots exactly twice", + sorted(i for q in GATE.G_CALLS for i in q[:4]) == + sorted(list(range(16)) * 2)) + record("A", "the 8 G-calls consume message indices 0..15 exactly once", + sorted(i for q in GATE.G_CALLS for i in q[4:]) == list(range(16))) + + def tamper_gcalls(): + old = GATE.G_CALLS[4] + GATE.G_CALLS[4] = (0, 5, 10, 15, 9, 8) # mx/my swapped + bad = [tuple(x) for x in GATE.G_CALLS] != ora_calls + GATE.G_CALLS[4] = old + return bad + bites("A", "G_CALLS comparison", tamper_gcalls) + + # -- A3 rotation amounts and their ORDER, recovered from both sides ----- + def oracle_rot_amounts(): + seen = [] + orig = ORA.rotr + + def spy(x, n): + seen.append(n) + return orig(x, n) + ORA.rotr = spy + try: + ORA.g([0] * 4, 0, 1, 2, 3, 0, 0) + finally: + ORA.rotr = orig + return seen + + def bref_rot_amounts(): + seen = [] + orig = GATE.RotateRight + + def spy(x, n): + seen.append(n) + return orig(x, n) + GATE.RotateRight = spy + try: + GATE.bref_g([BitVec(f"a{i}", 32) for i in range(4)], 0, 1, 2, 3, + BitVec("mx", 32), BitVec("my", 32)) + finally: + GATE.RotateRight = orig + return seen + + ora_rots, bref_rots = oracle_rot_amounts(), bref_rot_amounts() + record("A", "bref_g rotation amounts and order == oracle g", + ora_rots == bref_rots == [16, 12, 8, 7], f"{bref_rots}") + + # -- A4 differential: bref_* vs the oracle on concrete values ----------- + rng = random.Random(0xB1A3E) + + def w32(v): + return BitVecVal(v & MASK32, 32) + + def as_int(bv): + return simplify(bv).as_long() + + def diff_g(n): + for _ in range(n): + st = [rng.randrange(1 << 32) for _ in range(4)] + mx, my = rng.randrange(1 << 32), rng.randrange(1 << 32) + ref = list(st) + ORA.g(ref, 0, 1, 2, 3, mx, my) + bv = [w32(x) for x in st] + GATE.bref_g(bv, 0, 1, 2, 3, w32(mx), w32(my)) + if [as_int(x) for x in bv] != ref: + return False, (st, mx, my) + return True, None + + ok, cex = diff_g(300) + record("A", "bref_g == oracle g (300 random + carry/rotate edge inputs)", ok, + "" if ok else f"counterexample {cex}") + + # edge cases: all-zero, all-ones, single bits (exercise every carry and + # every rotate boundary) + edges = [[0] * 4, [MASK32] * 4, [1, 0, 0, 0], [0, 0, 0, MASK32], + [0x80000000] * 4, [0x0000FFFF, 0xFFFF0000, 0xF0F0F0F0, 0x0F0F0F0F]] + ok_edge = True + for st in edges: + for msg in ([0, 0], [MASK32, MASK32], [0x80000000, 1]): + ref = list(st) + ORA.g(ref, 0, 1, 2, 3, msg[0], msg[1]) + bv = [w32(x) for x in st] + GATE.bref_g(bv, 0, 1, 2, 3, w32(msg[0]), w32(msg[1])) + ok_edge &= ([as_int(x) for x in bv] == ref) + record("A", "bref_g == oracle g (edge inputs: 0, 2^32-1, MSB, split words)", + ok_edge) + + def diff_round(n): + for _ in range(n): + st = [rng.randrange(1 << 32) for _ in range(16)] + m = [rng.randrange(1 << 32) for _ in range(16)] + ref = list(st) + ORA.round_fn(ref, m) + got = GATE.bref_round_only([w32(x) for x in st], [w32(x) for x in m]) + if [as_int(x) for x in got] != ref: + return False + return True + record("A", "bref_round_only == oracle round_fn (60 random states+messages)", + diff_round(60)) + + m0 = [rng.randrange(1 << 32) for _ in range(16)] + record("A", "bref_permute == oracle permute (and is index-identical)", + [as_int(x) for x in GATE.bref_permute([w32(x) for x in m0])] + == ORA.permute(m0)) + + def diff_compress(n, rounds_list): + for _ in range(n): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + bl = rng.randrange(65) + fl = rng.randrange(128) + for r in rounds_list: + ref = ORA.compress(h, m, t, bl, fl, rounds=r) + got = GATE.bref_compress([w32(x) for x in h], [w32(x) for x in m], + w32(t & MASK32), w32((t >> 32) & MASK32), + w32(bl), w32(fl), r) + if [as_int(x) for x in got] != ref: + return False, (h, m, t, bl, fl, r) + return True, None + + ok, cex = diff_compress(25, [0, 1, 2, 5, 6, 7, 8]) + record("A", "bref_compress == oracle compress (25 vectors x rounds " + "{0,1,2,5,6,7,8}) — pins the rounds parameterisation", ok, + "" if ok else f"counterexample {cex}") + + # counters straddling 2^32 — the split order t_lo=v[12], t_hi=v[13] + ok_ctr = True + for t in (0, 1, 2**32 - 1, 2**32, 2**32 + 1, 2**47 + 12345, 2**64 - 1): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + ref = ORA.compress(h, m, t, 64, 3, rounds=6) + got = GATE.bref_compress([w32(x) for x in h], [w32(x) for x in m], + w32(t & MASK32), w32((t >> 32) & MASK32), + w32(64), w32(3), 6) + ok_ctr &= ([as_int(x) for x in got] == ref) + record("A", "counter split order matches across 2^32 " + "(t_lo->v[12], t_hi->v[13]; 7 counters incl. 2^32-1, 2^32)", ok_ctr) + + def tamper_bref_perm(): + old = GATE.MSG_PERMUTATION[:] + GATE.MSG_PERMUTATION[0], GATE.MSG_PERMUTATION[1] = old[1], old[0] + bad = not diff_compress(3, [2, 6])[0] + GATE.MSG_PERMUTATION[:] = old + return bad + bites("A", "bref_compress differential (permutation)", tamper_bref_perm) + + def tamper_bref_iv(): + old = GATE.IV[0] + GATE.IV[0] ^= 1 + bad = not diff_compress(2, [1, 6])[0] + GATE.IV[0] = old + return bad + bites("A", "bref_compress differential (IV)", tamper_bref_iv) + + # -- A5 how many times the permutation is applied ---------------------- + def ora_permute_count(rounds): + n = [0] + orig = ORA.permute + + def spy(m): + n[0] += 1 + return orig(m) + ORA.permute = spy + try: + ORA.compress([0] * 8, [0] * 16, 0, 64, 0, rounds=rounds) + finally: + ORA.permute = orig + return n[0] + + def bref_permute_count(rounds): + n = [0] + orig = GATE.bref_permute + + def spy(m): + n[0] += 1 + return orig(m) + GATE.bref_permute = spy + try: + GATE.bref_compress([w32(0)] * 8, [w32(0)] * 16, w32(0), w32(0), + w32(0), w32(0), rounds) + finally: + GATE.bref_permute = orig + return n[0] + + counts = [(r, ora_permute_count(r), bref_permute_count(r)) for r in range(9)] + record("A", "permute applications per rounds r == max(r-1,0), oracle == bref " + "(the classic off-by-one)", + all(o == b == max(r - 1, 0) for r, o, b in counts), + " ".join(f"r{r}:{b}" for r, _, b in counts)) + + # -- A6 the initial-state layout, probed slot by slot ------------------- + # rounds=0 makes out[i]=v[i]^v[i+8] and out[i+8]=v[i+8]^h[i] read the + # initial state directly, so each slot is individually observable. + h = [0] * 8 + m = [0] * 16 + tlo, thi, bl, fl = 0xA1A2A3A4, 0xB1B2B3B4, 0xC1C2C3C4, 0xD1D2D3D4 + out0 = [as_int(x) for x in GATE.bref_compress( + [w32(x) for x in h], [w32(x) for x in m], w32(tlo), w32(thi), + w32(bl), w32(fl), 0)] + layout_ok = ( + out0[0] == GATE.IV[0] and out0[1] == GATE.IV[1] and + out0[2] == GATE.IV[2] and out0[3] == GATE.IV[3] and + out0[4] == tlo and out0[5] == thi and out0[6] == bl and out0[7] == fl and + out0[8] == GATE.IV[0] and out0[12] == tlo and out0[13] == thi) + record("A", "initial v layout: v[8..12]=IV, v[12]=t_lo, v[13]=t_hi, " + "v[14]=block_len, v[15]=flags (probed slot by slot)", layout_ok, + f"out[4..8]={[hex(x) for x in out0[4:8]]}") + + hh = [0x11111111 * (i + 1) for i in range(8)] + out1 = [as_int(x) for x in GATE.bref_compress( + [w32(x) for x in hh], [w32(x) for x in m], w32(0), w32(0), w32(0), + w32(0), 0)] + ff_ok = (all(out1[i] == (hh[i] ^ [GATE.IV[0], GATE.IV[1], GATE.IV[2], + GATE.IV[3], 0, 0, 0, 0][i]) + for i in range(8)) and + all(out1[i + 8] == ([GATE.IV[0], GATE.IV[1], GATE.IV[2], GATE.IV[3], + 0, 0, 0, 0][i] ^ hh[i]) for i in range(8))) + record("A", "feed-forward: out[i]=v[i]^v[i+8] and out[i+8]=v[i+8]^h[i] " + "with h the ORIGINAL input CV (not the mutated state)", ff_ok) + + +# =========================================================================== +# SECTION B — transcription (b): DESIGN.md -> build_g / build_round / +# build_compress. Structural, by instrumenting the model. +# =========================================================================== +class Traced(GATE.Circuit): + """Circuit subclass that records the SSA dataflow the model builds. + + Words are lists of z3 byte expressions; rotr16/rotr8 return the SAME byte + objects (they are relabels), so resolving an operand's bytes to their + producing word automatically follows a free rotation back to its source + XOR — which is exactly the provenance question DESIGN 4.3/4.4/5 raises. + """ + + def __init__(self, tag, bug=None): + super().__init__(tag, bug) + self.words = [] # wid -> {kind, cells, used} + self.owner = {} # byte-expr name -> wid + self.ops = [] # {kind, ins:[wid], outs:[wid], perm:[..]} + self._pending = [] + + # -- registration ------------------------------------------------------ + def fresh_word(self): + w = super().fresh_word() + wid = len(self.words) + self.words.append({"kind": "unassigned", "cells": w, "used": 4}) + for c in w: + self.owner[str(c)] = wid + self._pending.append(wid) + return w + + def const_word(self, val): + w = super().const_word(val) + wid = len(self.words) + self.words.append({"kind": "const", "cells": w, "used": 4}) + return w + + def _wids(self, word): + return sorted({self.owner[str(c)] for c in word if str(c) in self.owner}) + + def _op(self, kind, ins, out_kinds, perm=None): + outs = self._pending[:] + self._pending = [] + for wid, k in zip(outs, out_kinds): + self.words[wid]["kind"] = k + self.ops.append({"kind": kind, + "ins": [self._wids(w) for w in ins], + "in_words": ins, "outs": outs, "perm": perm}) + return outs + + # -- the operations under contract ------------------------------------ + def xor(self, A, B): + self._pending = [] + out = super().xor(A, B) + self._op("xor", [A, B], ["xor_out"]) + return out + + def add2(self, A, B, drop_bool=False): + self._pending = [] + out = super().add2(A, B, drop_bool) + self._op("add2", [A, B], ["add_out"]) + return out + + def add3(self, A, B, M, drop_bool=False): + self._pending = [] + out = super().add3(A, B, M, drop_bool) + self._op("add3", [A, B, M], ["add_out"]) + return out + + def rotr(self, A, n, wrong_amount=False): + self._pending = [] + out = super().rotr(A, n, wrong_amount) + # fresh_word order inside Circuit.rotr: sll_lo, sllc_lo, sll_hi, + # sllc_hi, Y (the first four are used two bytes wide) + outs = self._op("rotr", [A], ["sll", "sllc", "sll", "sllc", "rot_out"], + perm=n) + for wid in outs[:4]: + self.words[wid]["used"] = 2 + return out + + def rotr16(self, A): + out = super().rotr16(A) + self.ops.append({"kind": "relabel16", "ins": [self._wids(A)], + "in_words": [A], "outs": [], + "perm": [A.index(c) if c in A else None for c in out]}) + return out + + def rotr8(self, A): + out = super().rotr8(A) + self.ops.append({"kind": "relabel8", "ins": [self._wids(A)], + "in_words": [A], "outs": [], + "perm": [A.index(c) if c in A else None for c in out]}) + return out + + # -- provenance analysis ---------------------------------------------- + def xor_consumed(self): + """wids that appear as an operand of at least one ByteAlu[XOR].""" + s = set() + for op in self.ops: + if op["kind"] == "xor": + for group in op["ins"]: + s.update(group) + return s + + def unchecked(self): + """SSA words whose byte-range DESIGN 4.3/4.4/5 sources from a + downstream XOR, but which no XOR in this scope consumes.""" + xc = self.xor_consumed() + return [wid for wid, w in enumerate(self.words) + if w["kind"] in ("add_out", "rot_out") and wid not in xc] + + +def _build_one_g(cls=Traced, build=None): + cir = cls("aud") + v = [None] * 16 + va, vb, vc, vd = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + mx, my = cir.fresh_word(), cir.fresh_word() + for w in (va, vb, vc, vd, mx, my): + cir.words[cir.owner[str(w[0])]]["kind"] = "input" + v[0], v[1], v[2], v[3] = va, vb, vc, vd + (build or GATE.build_g)(cir, v, 0, 1, 2, 3, mx, my, None, False) + return cir, v, (va, vb, vc, vd, mx, my) + + +def _build_compress(rounds): + cir = Traced("audc") + h = [cir.fresh_word() for _ in range(8)] + m = [cir.fresh_word() for _ in range(16)] + tlo, thi, bl, fl = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + for w in h + m + [tlo, thi, bl, fl]: + cir.words[cir.owner[str(w[0])]]["kind"] = "input" + out = GATE.build_compress(cir, h, m, tlo, thi, bl, fl, rounds) + return cir, out, dict(h=h, m=m, tlo=tlo, thi=thi, bl=bl, fl=fl) + + +def section_B(slow): + print("\n" + "=" * 74) + print("B CIRCUIT TRANSCRIPTION — DESIGN.md -> build_g / build_round / " + "build_compress") + print("=" * 74) + + # -- B1 the free rotations are byte relabels, not BV rotates ----------- + cir = GATE.Circuit("rel") + A = cir.fresh_word() + n_before = cir.n + r16, r8 = cir.rotr16(A), cir.rotr8(A) + record("B", "rotr16 is the index relabel [b2,b3,b0,b1] on the SOURCE bytes " + "(object identity, DESIGN 4.2/7.6)", + [c is A[i] for c, i in zip(r16, (2, 3, 0, 1))] == [True] * 4) + record("B", "rotr8 is the index relabel [b1,b2,b3,b0] on the SOURCE bytes", + [c is A[i] for c, i in zip(r8, (1, 2, 3, 0))] == [True] * 4) + record("B", "the free rotations commit NO new columns and emit NO " + "constraints (DESIGN 3: 'produce no columns')", + cir.n == n_before and cir.C == []) + + s = Solver() + s.add(Or(cir.word32(r16) != RotateRight(cir.word32(A), 16), + cir.word32(r8) != RotateRight(cir.word32(A), 8))) + record("B", "and the relabels are VALUE-equal to RotateRight 16 / 8 " + "(z3, all 2^32 inputs)", s.check() == unsat) + + def tamper_relabel(): + w = GATE.Circuit("t") + B = w.fresh_word() + wrong = [B[1], B[2], B[3], B[0]] # rotr8 pattern used for 16 + s2 = Solver() + s2.add(w.word32(wrong) != RotateRight(w.word32(B), 16)) + return s2.check() == sat + bites("B", "relabel value check", tamper_relabel) + + # -- B2 range-check provenance: the load-bearing claim ------------------ + print("\n -- B2 where does each SSA word's byte range actually come from? --") + gcir, gv, _ = _build_one_g() + kinds = {} + for w in gcir.words: + kinds[w["kind"]] = kinds.get(w["kind"], 0) + 1 + record("B", "one G commits 56 byte-cells + 6 carry bits (DESIGN 3 table)", + sum(w["used"] for w in gcir.words + if w["kind"] in ("add_out", "xor_out", "sll", "sllc", "rot_out")) == 56 + and sum(1 for c in gcir.C if "Or" in str(c)[:3] or str(c).startswith("Or")) == 6, + f"cells={sum(w['used'] for w in gcir.words if w['kind'] not in ('input','unassigned','const'))}, " + f"bool-constraints={sum(1 for c in gcir.C if str(c).startswith('Or'))}") + record("B", "one G = 2 add3 + 2 add2 + 4 xor + 2 shift-rotations + " + "1 rotr16 + 1 rotr8 (DESIGN 2/5)", + [sum(1 for o in gcir.ops if o["kind"] == k) + for k in ("add3", "add2", "xor", "rotr", "relabel16", "relabel8")] + == [2, 2, 4, 2, 1, 1]) + + xc_g = gcir.xor_consumed() + add_outs = [wid for wid, w in enumerate(gcir.words) if w["kind"] == "add_out"] + record("B", "all FOUR add outputs of a G (A1, C1, A2, C2) are consumed by a " + "ByteAlu INSIDE the same G — so MAIN 0's byte declaration is " + "derivable for them (the class where the range check is " + "load-bearing; see section C)", + len(add_outs) == 4 and all(w in xc_g for w in add_outs)) + + unchecked_in_g = gcir.unchecked() + detail = ", ".join(f"w{wid}({gcir.words[wid]['kind']})" for wid in unchecked_in_g) + record("B", "INSIDE one G, exactly one SSA output has no ByteAlu consumer: " + "the final rotr7 result B2 (its range check lives in the NEXT " + "G / the feed-forward — outside MAIN 0's scope)", + len(unchecked_in_g) == 1 + and gcir.words[unchecked_in_g[0]]["kind"] == "rot_out" + and unchecked_in_g[0] == gcir.owner[str(gv[1][0])], + f"unchecked in G-scope: [{detail}]") + + ccir, cout, cin = _build_compress(6) + unchecked_full = ccir.unchecked() + record("B", "in the FULL 6-round compression every add/shift output IS " + "consumed by a ByteAlu[XOR] — DESIGN 5/7.4's premise verified " + "mechanically (the gate never checks it)", + unchecked_full == [], + f"{sum(1 for w in ccir.words if w['kind'] in ('add_out','rot_out'))} " + f"add/shift outputs, {len(unchecked_full)} unchecked") + other_rounds = {r: len(_build_compress(r)[0].unchecked()) for r in (1, 2, 7)} + record("B", "…and for ROUNDS = 1, 2 and 7 too, so the premise is a property " + "of the layout, not of the round count", + set(other_rounds.values()) == {0}, str(other_rounds)) + + xc = ccir.xor_consumed() + ins_xored = {k: all(ccir.owner[str(w[0])] in xc for w in + (cin[k] if isinstance(cin[k], list) and + isinstance(cin[k][0], list) else [cin[k]])) + for k in ("h", "m", "tlo", "thi", "bl", "fl")} + record("B", "DESIGN 4.7 input claim: h, t_lo, t_hi, block_len, flags each " + "feed an XOR; m does NOT (so m is the one input needing an " + "explicit AreBytes)", + ins_xored["h"] and ins_xored["tlo"] and ins_xored["thi"] + and ins_xored["bl"] and ins_xored["fl"] and not ins_xored["m"], + str(ins_xored)) + + # which XOR is h[i]'s range check? h[0..4] land in round-0 'a' slots, + # which G only ever uses as an ADD operand — so their sole ByteAlu is the + # UPPER feed-forward half, the half a CV-only caller (DESIGN 1.1) would + # naturally drop. + h_consumers = [] + for i in range(8): + wid = ccir.owner[str(cin["h"][i][0])] + cons = [oi for oi, o in enumerate(ccir.ops) + if o["kind"] == "xor" and any(wid in gp for gp in o["ins"])] + h_consumers.append(len(cons)) + ff_start = min(oi for oi, o in enumerate(ccir.ops) + if o["kind"] == "xor" and + any(ccir.owner[str(cin["h"][0][0])] in gp for gp in o["ins"])) + record("B", "h[0..4] have exactly ONE ByteAlu consumer (the upper " + "feed-forward out[i+8]=v[i+8]^h[i]) while h[4..8] have two " + "(they are round-0 'b' slots, so X2 xors them too)", + h_consumers == [1, 1, 1, 1, 2, 2, 2, 2], + f"consumer counts {h_consumers}") + record("B", "…and that single consumer really is a feed-forward XOR, not a " + "round XOR (it is among the last 24 ops of the circuit)", + ff_start >= len(ccir.ops) - 24, + f"op #{ff_start} of {len(ccir.ops)}") + + def tamper_provenance(): + """A G-variant in which an add output has NO ByteAlu consumer — + exactly the deviation DESIGN 7.4 warns about. The detector must see + it.""" + def build_g_leaky(cir, v, a, b, c, d, mx, my, bug, gflag): + v[a] = cir.add3(v[a], v[b], mx) + v[d] = cir.rotr16(cir.xor(v[d], v[a])) + v[c] = cir.add2(v[c], v[d]) + v[b] = cir.rotr(cir.xor(v[b], v[c]), 12) + v[a] = cir.add3(v[a], v[b], my) + v[d] = cir.rotr8(cir.xor(v[d], v[a])) + v[c] = cir.add2(v[c], v[d]) # C2: consumer removed below + v[b] = cir.rotr(cir.xor(v[b], v[a]), 7) # reads v[a], not v[c] + cir2, _, _ = _build_one_g(build=build_g_leaky) + return len(cir2.unchecked()) == 2 + bites("B", "range-check provenance detector", tamper_provenance) + + # -- B3 the message schedule as build_compress actually wires it ------- + print("\n -- B3 message indexing under permute^r --") + + def capture_wiring(rounds=7): + """What build_compress ACTUALLY feeds each G: the original message + column index, recovered by tagging the committed message words.""" + seen = [] + orig_bg = GATE.build_g + + def spy_g(cir, v, a, b, c, d, mx, my, bug, gflag): + seen.append((a, b, c, d, mx[0], my[0])) + return orig_bg(cir, v, a, b, c, d, mx, my, bug, gflag) + + GATE.build_g = spy_g + try: + cir3 = Traced("sched") + h3 = [cir3.fresh_word() for _ in range(8)] + m3 = [cir3.fresh_word() for _ in range(16)] + tg = {str(m3[i][0]): i for i in range(16)} + t3 = [cir3.fresh_word() for _ in range(4)] + GATE.build_compress(cir3, h3, m3, t3[0], t3[1], t3[2], t3[3], rounds) + finally: + GATE.build_g = orig_bg + wired = [[(tg.get(str(mx)), tg.get(str(my))) + for (_, _, _, _, mx, my) in seen[r * 8:(r + 1) * 8]] + for r in range(rounds)] + return wired, [q[:4] for q in seen] + + wired, quads = capture_wiring(7) + # what the ORACLE says round r must consume: permute^r applied to the + # identity schedule, then indexed by round_fn's own message positions + expected = [] + sched = list(range(16)) + for r in range(7): + expected.append([(sched[ix], sched[iy]) for (_, _, _, _, ix, iy) + in ora_G_CALLS()]) + sched = ORA.permute(sched) + record("B", "build_compress feeds every G the ORIGINAL message column " + "under permute^r, for all 7 rounds x 8 G-calls (56 index " + "pairs), matching the oracle's permutation composition", + wired == expected, + f"round0 {wired[0][:2]}... round6 {wired[6][:2]}...") + record("B", "the state quadruples build_round passes match the oracle's " + "round_fn quadruples in order, all 7 rounds", + quads == [tuple(c[:4]) for c in ora_G_CALLS()] * 7) + + def tamper_sched(): + old = GATE.MSG_PERMUTATION[:] + GATE.MSG_PERMUTATION[3], GATE.MSG_PERMUTATION[4] = old[4], old[3] + try: + w2, _ = capture_wiring(7) + finally: + GATE.MSG_PERMUTATION[:] = old + return w2 != expected + bites("B", "message-schedule index check", tamper_sched) + + def tamper_quads(): + old = GATE.G_CALLS[5] + GATE.G_CALLS[5] = (1, 6, 11, 12, 11, 10) # mx/my swapped + try: + w2, q2 = capture_wiring(7) + finally: + GATE.G_CALLS[5] = old + return w2 != expected + bites("B", "G-call wiring check", tamper_quads) + + # -- B4 what the model does NOT carry ---------------------------------- + print("\n -- B4 what the circuit model does not represent --") + src = open(os.path.join(HERE, "blake3-chip/z3_blake_verify.py")).read() + + # Every variable the model creates is 8 bits wide, and every constraint it + # emits is an equation or a carry booleanity. There is no range-check + # OBJECT, so "AreBytes present" and "AreBytes absent" are the same model. + widths = set() + for w in ccir.words: + widths.update(c.size() for c in w["cells"]) + kinds = {} + for c in ccir.C: + kinds[c.decl().name()] = kinds.get(c.decl().name(), 0) + 1 + record("B", "every committed cell the model creates is BitVec(...,8): " + "byte-ness is the DECLARATION, never a modelled lookup", + widths == {8}, f"cell widths {sorted(widths)}") + record("B", "and every emitted constraint is '=' (xor / sum / shift / " + "recombine) or 'or' (carry booleanity) — no range constraint " + "object exists to be present or absent", + set(kinds) <= {"=", "or"}, str(kinds)) + cls_body = src[src.index("# Chip circuit model"):src.index("def build_g")] + record("B", "inside the Circuit class, 'AreBytes' occurs only in comments " + "(the class header ':118-119' and rotr ':212') — the gate " + "DOCUMENTS the assumption ('Byte width == the ByteAlu/AreBytes " + "range-check contract') but has no object for it", + all(ln.strip().startswith("#") for ln in cls_body.split("\n") + if "AreBytes" in ln)) + record("B", "the model has NO mu column: every eval identity is asserted " + "ungated, which is exact for a live row and blind to padding " + "rows (DESIGN 4.5 / 7.1 are therefore outside the gate)", + not any("mu" in str(c).lower() for c in ccir.C) + and "mu" not in "".join(str(w["cells"][0]) for w in ccir.words)) + record("B", "the model has no bus / multiplicity / timestamp layer at all " + "(confirming README finding 1, not re-deriving it)", + not any(k in src for k in ("Multiplicity", "TIMESTAMP", "bus_interaction", + "receive(", "send("))) + nunused = sum(1 for w in ccir.words if w["kind"] in ("sll", "sllc")) + record("B", "each shift-rotation allocates 4 x fresh_word() but uses only " + "2 bytes of each (fresh_word()[:2]) — 8 free unconstrained BVs " + "per rotation, unread and harmless", + nunused == 4 * 96, f"{nunused} halfword slots over 96 rotations") + + +# =========================================================================== +# SECTION C — the dangerous direction, in the field: where byte-ness +# actually comes from, and the forgery the model cannot see. +# =========================================================================== +def _field_word(s, name, ranged): + cells = [Int(f"{name}_{i}") for i in range(4)] + for c in cells: + s.add(c >= 0, c < (256 if ranged else P)) + return cells, sum(cells[i] * 2**(8 * i) for i in range(4)) + + +def add_pinned(nops, out_ranged, ops_concrete=None, want_model=False): + """DESIGN 4.3/4.4 add, modelled in the FIELD. Is the committed sum word + pinned to (sum of operands) mod 2^32? unsat = pinned, sat = forgeable.""" + s = Solver() + ops = [] + for k in range(nops): + if ops_concrete: + ops.append(IntVal(ops_concrete[k])) + else: + _, v = _field_word(s, f"in{k}", True) + ops.append(v) + scells, S = _field_word(s, "S", out_ranged) + if nops == 2: + c = Int("c") + s.add(Or(c == 0, c == 1)) + csum = c + else: + c1, c2 = Int("c1"), Int("c2") + s.add(Or(c1 == 0, c1 == 1), Or(c2 == 0, c2 == 1)) + csum = c1 + c2 + s.add((sum(ops) - S - 2**32 * csum) % P == 0) + T, K = Int("T"), Int("K") + s.add(K >= 0, K <= nops - 1, T >= 0, T < 2**32, sum(ops) == K * 2**32 + T) + s.add((S - T) % P != 0) # a wrong FIELD VALUE, not just cells + res = s.check() + if res == sat and want_model: + mo = s.model() + g = lambda e: mo.eval(e, model_completion=True).as_long() + return res, dict(operands=[hex(g(o)) for o in ops], honest=hex(g(T)), + forged=hex(g(S) % P), cells=[g(x) for x in scells], + carries=g(csum)) + return res, None + + +def rot_pinned(r, kept, want_model=False): + """DESIGN 4.2 rotation in the FIELD, COMPOSED: both shift identities + + both recombine identities + the byte range on Y that the downstream + ByteAlu gives. `kept` = which halfwords carry their AreBytes bound.""" + s = Solver() + xlo, xhi = Int("xlo"), Int("xhi") + s.add(xlo >= 0, xlo < 2**16, xhi >= 0, xhi < 2**16) + hw = {} + for n in ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"): + if n in kept: + lo, hi = Int(n + "_b0"), Int(n + "_b1") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) + hw[n] = lo + 256 * hi + else: + v = Int(n) + s.add(v >= 0, v < P) + hw[n] = v + s.add((xlo * 2**r - hw["SLLC_lo"] * 2**16 - hw["SLL_lo"]) % P == 0) + s.add((xhi * 2**r - hw["SLLC_hi"] * 2**16 - hw["SLL_hi"]) % P == 0) + Y = [Int(f"Y{i}") for i in range(4)] + for y in Y: + s.add(y >= 0, y < 256) + Ylo, Yhi = Y[0] + 256 * Y[1], Y[2] + 256 * Y[3] + s.add((Ylo - hw["SLL_hi"] - hw["SLLC_lo"]) % P == 0) + s.add((Yhi - hw["SLL_lo"] - hw["SLLC_hi"]) % P == 0) + X = xlo + 2**16 * xhi + Q, R = Int("Q"), Int("R") + s.add(Q >= 0, Q < 2**r, R >= 0, R < 2**32, X * 2**r == Q * 2**32 + R) + wlo, whi = Int("wlo"), Int("whi") + s.add(wlo >= 0, wlo < 2**16, whi >= 0, whi < 2**16, R + Q == wlo + 2**16 * whi) + honest = whi + 2**16 * wlo + s.push() + s.add(Ylo + 2**16 * Yhi != honest) + res = s.check() + mdl = None + if res == sat and want_model: + mo = s.model() + g = lambda e: mo.eval(e, model_completion=True).as_long() + mdl = dict(X=hex(g(X)), honest_Y=hex(g(honest)), + forged_Y=hex(g(Ylo + 2**16 * Yhi)), + SLL_lo=hex(g(hw["SLL_lo"])), SLLC_lo=hex(g(hw["SLLC_lo"])), + SLL_hi=hex(g(hw["SLL_hi"])), SLLC_hi=hex(g(hw["SLLC_hi"]))) + s.pop() + # non-vacuity: the honest witness must satisfy the model + s.add(Ylo + 2**16 * Yhi == honest) + live = s.check() == sat + return res, mdl, live + + +def section_C(slow): + print("\n" + "=" * 74) + print("C FIELD-LEVEL — what the byte range checks actually buy, and the " + "forgery the\n BV model cannot express") + print("=" * 74) + + for n in (2, 3): + res, _ = add_pinned(n, True) + record("C", f"add{n}: WITH the output's byte range check the sum is " + f"pinned to (a+b{'+m' if n == 3 else ''}) mod 2^32, for " + f"ALL operands (symbolic, mod p)", res == unsat) + for n in (2, 3): + res, mdl = add_pinned(n, False, want_model=True) + record("C", f"add{n}: WITHOUT it the committed sum is FORGEABLE — the " + f"prover commits the UNREDUCED sum with carry 0", + res == sat, str(mdl)) + + res, mdl = add_pinned(2, False, ops_concrete=[0x80000000, 0x80000000], + want_model=True) + record("C", "concrete witness: a=b=0x80000000, honest s=0, forged s=2^32 " + "with carry=0 (cells [2^32,0,0,0]) — every modelled constraint " + "satisfied", res == sat and mdl["forged"] == hex(2**32), str(mdl)) + + # the gate cannot tell the two apart: its `s` is 4 BitVec(8)s either way + src = open(os.path.join(HERE, "blake3-chip/z3_blake_verify.py")).read() + record("C", "…and the gate models BOTH chips identically: add2/add3 return " + "`self.fresh_word()`, i.e. 4x BitVec(...,8), so the range check " + "is DECLARED, never derived from a modelled lookup", + "s = self.fresh_word()" in src and "AreBytes" not in + src[src.index("def add2"):src.index("def rotr")]) + + print("\n -- C2 the rotation, composed (the gate tests it in isolation) --") + lattice = {} + for r in (4, 9): + for k in range(4, -1, -1): + for kept in itertools.combinations( + ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"), k): + res, _, live = rot_pinned(r, set(kept)) + lattice[(r, kept)] = (res, live) + all_live = all(live for _, live in lattice.values()) + record("C", "non-vacuity: the honest rotation witness satisfies the " + "composed field model in all 32 bound configurations", + all_live) + record("C", "rotation with all four AreBytes bounds: Y is pinned to " + "rotr12/rotr7 for ALL 2^32 inputs (symbolic, mod p — the gate " + "only ever checked one concrete halfword in the field)", + lattice[(4, ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"))][0] == unsat + and lattice[(9, ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"))][0] == unsat) + one_sll = all(lattice[(r, k)][0] == unsat for r in (4, 9) + for k in (("SLL_lo",), ("SLL_hi",))) + no_sll = all(lattice[(r, k)][0] == sat for r in (4, 9) + for k in ((), ("SLLC_lo",), ("SLLC_hi",), ("SLLC_lo", "SLLC_hi"))) + record("C", "necessary AND sufficient bound set = at least one of " + "{SLL_lo, SLL_hi}; every configuration with neither is " + "forgeable, every configuration with either is pinned — the " + "SLLC bounds are not load-bearing at all", + one_sll and no_sll) + res, mdl, _ = rot_pinned(9, set(("SLLC_lo", "SLLC_hi")), want_model=True) + record("C", "the composed rotation forgery (both SLL bounds dropped) exists " + "for exactly ONE input, X=0xFFFFFFFF -> forged Y=0 instead of " + "0xFFFFFFFF — not 'any input', as the gate's isolated control " + "suggests", res == sat and mdl["X"] == hex(0xFFFFFFFF), str(mdl)) + + # exhaustively: is X = 0xFFFFFFFF the only one? + def enumerate_bad_X(r, limit=4): + found = [] + seen = set() + for _ in range(limit): + s = Solver() + xlo, xhi = Int("xlo"), Int("xhi") + s.add(xlo >= 0, xlo < 2**16, xhi >= 0, xhi < 2**16) + hw = {} + for n in ("SLL_lo", "SLL_hi"): + v = Int(n) + s.add(v >= 0, v < P) + hw[n] = v + for n in ("SLLC_lo", "SLLC_hi"): + lo, hi = Int(n + "_b0"), Int(n + "_b1") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) + hw[n] = lo + 256 * hi + s.add((xlo * 2**r - hw["SLLC_lo"] * 2**16 - hw["SLL_lo"]) % P == 0) + s.add((xhi * 2**r - hw["SLLC_hi"] * 2**16 - hw["SLL_hi"]) % P == 0) + Y = [Int(f"Y{i}") for i in range(4)] + for y in Y: + s.add(y >= 0, y < 256) + Ylo, Yhi = Y[0] + 256 * Y[1], Y[2] + 256 * Y[3] + s.add((Ylo - hw["SLL_hi"] - hw["SLLC_lo"]) % P == 0) + s.add((Yhi - hw["SLL_lo"] - hw["SLLC_hi"]) % P == 0) + X = xlo + 2**16 * xhi + Q, R = Int("Q"), Int("R") + s.add(Q >= 0, Q < 2**r, R >= 0, R < 2**32, X * 2**r == Q * 2**32 + R) + wlo, whi = Int("wlo"), Int("whi") + s.add(wlo >= 0, wlo < 2**16, whi >= 0, whi < 2**16, + R + Q == wlo + 2**16 * whi) + s.add(Ylo + 2**16 * Yhi != whi + 2**16 * wlo) + for x in seen: + s.add(X != x) + if s.check() != sat: + break + xv = s.model().eval(X, model_completion=True).as_long() + seen.add(xv) + found.append(hex(xv)) + return found + bad4, bad9 = enumerate_bad_X(4), enumerate_bad_X(9) + record("C", "exhaustive: X=0xFFFFFFFF is the ONLY forgeable input for both " + "r=4 and r=9 (all other X enumerated away -> unsat)", + bad4 == bad9 == ["0xffffffff"], f"r=4 {bad4} r=9 {bad9}") + + # …but the rotation OUTPUT does not need its own byte range check: both + # recombine identities together pin its VALUE regardless of how its cells + # decompose. So the free-range-check argument is load-bearing for the add + # outputs and for one SLL per rotation — and for nothing else. + s = Solver() + xlo, xhi = Int("xlo"), Int("xhi") + s.add(xlo >= 0, xlo < 2**16, xhi >= 0, xhi < 2**16) + hw = {} + for n in ("SLL_lo", "SLLC_lo", "SLL_hi", "SLLC_hi"): + lo, hi = Int(n + "_b0"), Int(n + "_b1") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) + hw[n] = lo + 256 * hi + r = 9 + s.add((xlo * 2**r - hw["SLLC_lo"] * 2**16 - hw["SLL_lo"]) % P == 0) + s.add((xhi * 2**r - hw["SLLC_hi"] * 2**16 - hw["SLL_hi"]) % P == 0) + Ycells = [Int(f"Yf{i}") for i in range(4)] + for c in Ycells: + s.add(c >= 0, c < P) # NO range check on Y + Ylo, Yhi = Ycells[0] + 256 * Ycells[1], Ycells[2] + 256 * Ycells[3] + s.add((Ylo - hw["SLL_hi"] - hw["SLLC_lo"]) % P == 0) + s.add((Yhi - hw["SLL_lo"] - hw["SLLC_hi"]) % P == 0) + X = xlo + 2**16 * xhi + Q, R = Int("Q"), Int("R") + s.add(Q >= 0, Q < 2**r, R >= 0, R < 2**32, X * 2**r == Q * 2**32 + R) + wlo, whi = Int("wlo"), Int("whi") + s.add(wlo >= 0, wlo < 2**16, whi >= 0, whi < 2**16, R + Q == wlo + 2**16 * whi) + s.add((Ylo + 2**16 * Yhi - (whi + 2**16 * wlo)) % P != 0) + record("C", "the rotation OUTPUT needs no range check of its own: the two " + "recombine identities pin its value even with free field cells " + "— so the 'free range check' is load-bearing only for the add " + "outputs and one SLL halfword per rotation", s.check() == unsat) + + print("\n -- C3 the width audit's two claims, re-derived symbolically --") + # the gate proves each on ONE concrete input; prove them for all inputs + s = Solver() + inhw = Int("in_hw") + s.add(inhw >= 0, inhw < 2**16) + lo, hi = Int("lo"), Int("hi") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) + SLL = lo + 256 * hi + SLLC = Int("SLLC") + s.add(SLLC >= 0, SLLC < 2**16) + r = 9 + s.add((inhw * 2**r - SLLC * 2**16 - SLL) % P == 0) + ref = Int("ref") + s.add(ref >= 0, ref < 2**16, (inhw * 2**r - ref) % 2**16 == 0) + s.add(SLL != ref) + record("C", "field_shift_bound's UNSAT holds for ALL in_hw, not just " + "0x9C3A (the gate tests one point)", s.check() == unsat) + + s = Solver() + a, b, m3 = Int("a"), Int("b"), Int("m") + for x in (a, b, m3): + s.add(x >= 0, x < 2**32) + S = Int("S") + s.add(S >= 0, S < 2**32) + c1, c2 = Int("c1"), Int("c2") + s.add(Or(c1 == 0, c1 == 1), Or(c2 == 0, c2 == 1)) + s.add((a + b + m3 - S - 2**32 * (c1 + c2)) % P == 0) + K, T = Int("K"), Int("T") + s.add(K >= 0, K <= 2, T >= 0, T < 2**32, a + b + m3 == K * 2**32 + T) + s.add(S != T) + record("C", "field_add_carry's UNSAT holds for ALL (a,b,m), not just " + "3x0xF0000000", s.check() == unsat) + + # dropping the booleanity really does free s, composed with s's byte range + s = Solver() + a, b, m3 = IntVal(0x12345678), IntVal(0x9ABCDEF0), IntVal(0x0F0F0F0F) + scells, S = _field_word(s, "S", True) # s STILL byte-range-checked + k = Int("k") + s.add(k >= 0, k < P) # booleanity dropped + s.add((a + b + m3 - S - 2**32 * k) % P == 0) + honest = (0x12345678 + 0x9ABCDEF0 + 0x0F0F0F0F) % 2**32 + s.add(S != honest) + res = s.check() + mdl = None + if res == sat: + mo = s.model() + mdl = dict(honest=hex(honest), + forged=hex(mo.eval(S, model_completion=True).as_long()), + k=mo.eval(k, model_completion=True).as_long()) + record("C", "dropping the carry booleanity is forgeable even WITH the byte " + "range check on s (so this control is faithful to the composed " + "chip, unlike the shift one)", res == sat, str(mdl)) + + print("\n -- C4 the message columns (DESIGN 4.7): AreBytes vs the model --") + # without AreBytes on m the cells bind only sum(m_i 2^8i): exhibit two + # distinct cell vectors that satisfy every constraint identically. + honest_cells = [0x9A, 0x00, 0x13, 0x7F] + forged_cells = [0x9A + 256, 0x00 - 1, 0x13, 0x7F] + same_value = (sum(honest_cells[i] * 2**(8 * i) for i in range(4)) % P == + sum(forged_cells[i] * 2**(8 * i) for i in range(4)) % P) + record("C", "without the explicit AreBytes, a message word has many cell " + "representations with the same value (here [0x9A,0,0x13,0x7F] " + "and [0x19A,-1,0x13,0x7F] = [.., p-1, ..]): the chip binds " + "sum(m_i 2^8i), not the 64 bytes", same_value, + f"forged cells over F_p: {[c % P for c in forged_cells]}") + record("C", "the gate declares m as 16 x 4 BitVec(...,8), so it proves the " + "SAME UNSAT for a chip with and without those 32 AreBytes sends", + "m = [cir.fresh_word() for _ in range(16)]" in src) + + +# =========================================================================== +# SECTION D — gate hygiene: are the UNSATs non-vacuous, and is the model's +# carry encoding the one DESIGN.md specifies? +# =========================================================================== +def section_D(slow): + print("\n" + "=" * 74) + print("D GATE HYGIENE") + print("=" * 74) + + cir, v, _ = _build_one_g() + s = Solver() + s.add(And(*cir.C)) + record("D", "the G circuit's constraint set is SATISFIABLE on its own — " + "MAIN 0's UNSAT is not vacuous", s.check() == sat) + counts = {} + for opname, call in (("xor", lambda c: c.xor(c.fresh_word(), c.fresh_word())), + ("add2", lambda c: c.add2(c.fresh_word(), c.fresh_word())), + ("add3", lambda c: c.add3(c.fresh_word(), c.fresh_word(), + c.fresh_word())), + ("rotr12", lambda c: c.rotr(c.fresh_word(), 12)), + ("rotr16", lambda c: c.rotr16(c.fresh_word()))): + c = GATE.Circuit("cnt") + call(c) + counts[opname] = len(c.C) + record("D", "constraint counts per op match DESIGN 4.1-4.4: xor 4 (pure " + "lookup, modelled as 4 byte equalities), add2 2 (sum + 1 " + "booleanity), add3 3 (sum + 2 booleanities), rotr12 4 " + "(2 shift + 2 recombine), rotr16 0 (free relabel)", + counts == {"xor": 4, "add2": 2, "add3": 3, "rotr12": 4, "rotr16": 0}, + str(counts)) + + ccir, cout, _ = _build_compress(6) + s = Solver() + s.add(And(*ccir.C)) + record("D", "the 6-round circuit's constraint set is SATISFIABLE on its own", + s.check() == sat) + + # DESIGN 4.3 commits NO carry column for the 2-op add (carry is the linear + # expression (a+b-s)*2^-32); the model commits a boolean column instead. + # Prove the two are equivalent. + s = Solver() + A, B, S = Int("A"), Int("B"), Int("S") + for x in (A, B, S): + s.add(x >= 0, x < 2**32) + c_derived = Int("cd") + lhs = Or(And((A + B - S - 2**32 * 0) % P == 0), + And((A + B - S - 2**32 * 1) % P == 0)) # committed-boolean form + rhs = ((A + B - S) * pow(2**32, -1, P) % P == 0) + # derived form: carry := (A+B-S)*2^-32 mod p, booleanity carry*(carry-1)=0 + cd = ((A + B - S) * pow(2**32, -1, P)) % P + rhs = Or(cd == 0, cd == 1) + s.add(lhs != rhs) + record("D", "DESIGN 4.3's DERIVED carry (linear expr x INV_SHIFT_32, " + "booleanity) and the model's COMMITTED boolean carry are " + "equivalent over F_p — the difference is 1 column per add2, " + "not a semantic one", s.check() == unsat) + + note("DESIGN 4.8's ledger row for the recombine identity says body degree " + "2 -> 3 after x mu; the body mu*(Ylo - SLL_hi - SLLC_lo) is LINEAR in " + "committed columns, so it is 1 -> 2. Over-stated in the safe " + "direction; the 'no constraint exceeds 3' verdict is unaffected.") + note("DESIGN 3's per-G table counts 1 committed carry bit for each add2, " + "while DESIGN 4.3 makes that carry a DERIVED linear expression " + "(a+b-s)*INV_SHIFT_32 with no column. The gate models the committed " + "form. Equivalent as constraints (proved above); the two readings " + "differ by 96 cells/compression in the DESIGN 6 cost table.") + + # The positive controls are the gate's only external anchor. They pin the + # circuit's output to a RECORDED vector, so a stale fixture would silently + # anchor the gate to nothing. + vecs = GATE.load_canonical_6round() + ok_vec = all(ORA.compress_6round(v["h"], v["m"], v["t"], v["block_len"], + v["flags"]) == v["out"] for v in vecs) + record("D", f"all {len(vecs)} canonical 6-round fixture vectors reproduce " + "from the oracle's compress_6round — the positive controls " + "anchor to the live oracle, not a stale file", ok_vec) + record("D", "…and they are genuinely 6-round: none of them equals the " + "7-round compression of the same input", + all(ORA.compress(v["h"], v["m"], v["t"], v["block_len"], v["flags"], + rounds=7) != v["out"] for v in vecs)) + h7, m7, tlo7, thi7, bl7, fl7, out7 = GATE.gen_7round_vector() + record("D", "gen_7round_vector's output is the oracle's 7-round " + "compression of its own inputs", + ORA.compress(h7, m7, tlo7 | (thi7 << 32), bl7, fl7, rounds=7) == out7) + + # WIDE = 48 must be wide enough that the BV identities are integer + # identities, and small-enough values that they coincide with mod-p. + s = Solver() + a, b, m3, S = Int("a"), Int("b"), Int("m"), Int("S") + for x in (a, b, m3, S): + s.add(x >= 0, x < 2**32) + c1, c2 = Int("c1"), Int("c2") + s.add(Or(c1 == 0, c1 == 1), Or(c2 == 0, c2 == 1)) + s.add((a + b + m3 == S + 2**32 * (c1 + c2)) != + (((a + b + m3 - S - 2**32 * (c1 + c2)) % P) == 0)) + record("D", "the 3-op sum identity over Z (what WIDE=48 BV computes) and " + "over F_p (what the chip computes) are equivalent under the " + "byte bounds — no wraparound is available on either side, " + "confirming DESIGN 7.9", s.check() == unsat) + # The gate ASSUMES the BITWISE contracts (assume-guarantee). They are + # cheap to verify against the real table, so verify them. + bw = os.path.join(HERE, "..", "..", "prover", "src", "tables", "bitwise.rs") + if os.path.exists(bw): + rs = open(bw).read() + record("D", "the assumed ByteAlu[XOR] contract is real: bitwise.rs " + "enumerates x,y in 0..256 and sets cols::XOR = x^y, and the " + "receiver pins (XOR, X, Y) -> XOR", + "for x in 0u32..256 {" in rs and "for y in 0u32..256 {" in rs + and "table.set_byte(row_idx, cols::XOR, (x ^ y) as u8);" in rs + and "Multiplicity::Column(cols::MU_BYTE_ALU_XOR)" in rs) + record("D", "the assumed AreBytes contract is real: an AreBytes " + "receiver over the same 0..256 x 0..256 domain", + "BusId::AreBytes," in rs + and "ARE_BYTES[X, Y] - range check two byte values" in rs) + else: + note(f"bitwise.rs not found at {bw}; the ByteAlu/AreBytes contracts " + "were not cross-checked in this run.") + + note("block_len and flags are modelled as free 32-bit words; the design " + "says 0..64 and 0..127. The model is WEAKER there, which is the safe " + "direction, and DESIGN.md specifies no such constraint either.") + note("DESIGN 4.1 allows a ByteAlu operand to be a linear combination of " + "cells ('sum <= 255'); the model never uses one — rotr16/rotr8 are " + "pure index relabels, so every operand is a single cell. Consistent, " + "but the linear-combo contract is therefore unexercised.") + + if slow: + print("\n -- D2 the gate's own BV verdicts, re-run --") + record("D", "check_g() == unsat (MAIN 0)", GATE.check_g() == unsat) + record("D", "check_compress(0) == unsat (MAIN 1)", + GATE.check_compress(0) == unsat) + record("D", "check_g(bug='swap_g_operand') == sat", + GATE.check_g(bug="swap_g_operand") == sat) + + +# =========================================================================== +def main(): + slow = "--slow" in sys.argv + print("=" * 74) + print("BLAKE3 GATE TRANSCRIPTION AUDIT — regression suite") + print("=" * 74) + section_A(slow) + section_B(slow) + section_C(slow) + section_D(slow) + + print("\n" + "=" * 74) + fails = [(s, n) for s, n, ok, _ in RESULTS if not ok] + print(f"SUMMARY: {len(RESULTS) - len(fails)}/{len(RESULTS)} checks pass") + for s, n in fails: + print(f" FAIL [{s}] {n}") + print("=" * 74) + sys.exit(1 if fails else 0) + + +if __name__ == "__main__": + main() diff --git a/thoughts/blake3/blake3-chip/DESIGN.md b/thoughts/blake3/blake3-chip/DESIGN.md new file mode 100644 index 000000000..9a04d72f5 --- /dev/null +++ b/thoughts/blake3/blake3-chip/DESIGN.md @@ -0,0 +1,489 @@ +# BLAKE3 compression chip — constraint-system & bus design (Phase 2) + +**Status.** Model-level design + z3 equivalence gate, done **before** any Rust. +Ground truth = the phase-1 oracle (`../blake3-oracle/`, VALIDATED against 3 +external anchors). Cost model = the verified one in `../keccak-verify/tier2_cost_model.md` +(a committed cell is expensive; each bus send ≈ 1.5 base cells of aux; **hard** +max constraint degree 3 *including* the ×μ gating factor). + +> **⚠ Every `../keccak-verify/` citation in this document is DEAD.** That +> directory lived in the same 2026-07-23 session scratchpad this design was +> recovered from and was never committed — it exists on no branch. Two +> conclusions were deferred to it, and both have since been re-established +> independently, so nothing here rests on the missing files: +> * the **cost model** above (1.5 aux cells/send, degree ≤ 3 incl. ×μ) — the +> per-G and per-compression arithmetic in §2/§3/§6 was recomputed from +> scratch and checks out; +> * the **shift-identity bound necessity** cited at §4.2 and §9 — re-derived +> symbolically over all 2^32 inputs by the 2026-07-29 transcription audits, +> which is stronger than the single-point check the lost file made. +> +> Do not go looking for them; read the citations as historical. + +**Verdict (numbers derived below, gate in `z3_blake_verify.py`):** +* **Layout: B — one row per compression, fully unrolled.** Chosen by arithmetic + (≈5,030 cell-equiv vs ≈5,510 for one-row-per-round), and it deletes the + state+message handoff bus entirely. Table below. +* **O1 (3-operand add carry): option (c) — two summed carry bits.** Cheaper than + both options the oracle listed and stays degree ≤3 after μ-gating. +* **Rotations: rotr16/rotr8 free (byte relabel); rotr12/rotr7 inlined** as the + μ-gated linear shift identity (no HWSL sends), saving 4 sends/G. +* **Every eval constraint is μ-gated, padding is all-zero, every constraint ≤3.** +* **≈5,030 cell-equiv per 6-round compression (≈5,810 for 7-round)** — about + **1/15 of a keccak-f permutation** (≈77,000). + +--- + +## 1. Scope & I/O interface + +The chip implements the compression function `f` (oracle §2.4), **not** the tree. +Primary target is the **6-round internal variant** (Merkle 2-to-1 / Fiat–Shamir); +the design is `ROUNDS`-parameterised so 7-round is the same layout with one more +unrolled round. + +### 1.1 Lean internal interface (the one we build first): 2-to-1 compression + +Exposed on a dedicated **`Blake3` bus**. A parent-node caller supplies the two +child chaining values as the message and reads back the truncated CV. + +**Receive** (multiplicity μ) — the compression inputs: + +| field | words | bytes | source | +|---|---|---|---| +| `h[0..8]` chaining value / key | 8 | 32 | caller | +| `m[0..16]` message = `left_cv ‖ right_cv` | 16 | 64 | caller | +| `t_lo, t_hi` counter split | 2 | 8 | caller (t=0 for parents) | +| `block_len` | 1 | 4 | caller (64 for parents) | +| `flags` | 1 | 4 | caller (PARENT ∣ … for parents) | + +**Send** (multiplicity μ) — the output `out[0..16]` (16 words = 64 B). CV-only +call sites read `out[0:8]`; the chip always produces all 16 (the XOF root needs +them, oracle §2.4). + +**Both tuples MUST lead with `TIMESTAMP_0, TIMESTAMP_1` — this is mandatory, +not optional.** The receive and the send are two separate interactions, so +without a key present in *both* nothing ties a row's inputs to its own outputs. +With two compressions in a trace a prover could then have row A receive +`inputs_A` and send `out_B` while row B does the reverse: every tuple still +appears exactly once on each side, **so the bus balances**, and both callers +read a result that is not the compression of their own input. + +This is not a hypothetical hardening: `keccak.rs` — the chip this design copies +its I/O idiom from (§1.2) — carries `TIMESTAMP_0, TIMESTAMP_1` in *both* halves +of its internal `Keccak` bus (send at `round = 0`, receive at `round = 24`) +for exactly this reason. Do not deviate from it. + +**The gate cannot check this.** `z3_blake_verify.py` models arithmetic only and +has no bus-interaction layer at all, so a missing binding leaves every UNSAT on +the board unchanged. It has to be got right by construction. + +`IV[0..4]` (v[8..11]) are **compile-time constants inlined** into the round-0 +arithmetic — not columns, not on the bus. + +### 1.2 General syscall / memory variant (sketched, not built here) + +Same core; replaces the internal `Blake3` receive/send with the keccak I/O +idiom (`prover/src/tables/keccak.rs:160-449`): an `Ecall` receiver binding +(timestamp, syscall#), a `Memw` read of `x10` binding the state pointer, then +per-word `Memw` reads/writes of `h`,`m`,`t`,`block_len`,`flags`,`out`. Adds +~1 Ecall + ~(112+64)/8 ≈ 22 Memw interactions and the pointer-arith columns; +**orthogonal to the mixing core designed here** (open questions O5/O6 live here). + +--- + +## 2. Row-layout decision (by arithmetic) + +Per-compression work (6 rounds): each round = 8 G-functions; each G = **2 +three-operand adds, 2 two-operand adds, 4 XORs, 2 free rotations (rotr16/8), +2 shift rotations (rotr12/7)**. Committed cells and bus sends per G (SSA form, +derivation in §5): + +* committed: **56 byte-cells + 6 carry-bit cells** per G +* sends: **24** per G (16 ByteAlu[XOR] + 8 AreBytes for the two shift rotations) + +| per compression | **A: 1 row / round (6 rows)** | **B: unrolled (1 row)** | +|---|---:|---:| +| logic committed (8 G × 6) | 2,976 | 2,976 | +| feed-forward committed | 64 | 64 | +| I/O input columns | 112 (×6 carried!) = 672 | 112 (once) | +| state+message handoff columns | +128 B/row × 6 = 768 | 0 | +| round-index / selector bookkeeping | ~18 | 0 | +| **committed total** | **≈ 3,760** | **≈ 3,150** | +| bus sends N (logic 192/round) | 1,152 + 6 handoff + 32 msg-rc ≈ 1,190 | 1,152 + 64 ff + 34 I/O = 1,250 | +| **aux = 3·⌈N/2⌉** | **≈ 1,750** | **≈ 1,875** | +| **total cell-equiv** | **≈ 5,510** | **≈ 5,030** | +| handoff bus | `Blake3Round` carries state(64B)+**msg**(64B)/row | none | +| structural cost | per-row state+msg reconstruction, permute-on-bus | pure compile-time wiring | + +**Decision: B.** It wins on total cells (the handoff re-commits the 16-word +state *and* the 16-word message on every one of the 6 rows — BLAKE3, unlike +keccak, must carry the message down the rounds, which is the single biggest +extra cost of A) and it is structurally far simpler: the message schedule is a +compile-time permutation, so unrolling makes every round reference the original +16 committed message words under `permute^r` with **zero** runtime handoff. The +concentration of all sends into one row makes B's aux marginally higher, but the +committed-column saving dominates. B also removes round-index bookkeeping and the +`Blake3Round` bus wholesale. (Matches the oracle's recommendation, now with the +numbers behind it.) + +Only reason to revisit A: if the ~3,150-wide single row's LDE/Merkle width ever +dominates trace area for tiny proofs — not the case here (keccak's per-row width +is already ~1,480+aux and BLAKE3 has 1/4 the rounds). + +--- + +## 3. Column layout (Layout B) + +One row = one compression call. Names group by role; counts are for `ROUNDS=6`. +"SSA word" = a fresh 4-byte committed word produced by one op. + +| block | columns | count | notes | +|---|---|---:|---| +| `TIMESTAMP_0/1` | 2 | 2 | bus binding — **mandatory in both the receive and the send** (§1.1); omitting it lets two rows swap outputs with the bus still balancing | +| `MU` | 1 | 1 | multiplicity / gate flag | +| `H[0..8]` | 8 words | 32 | input CV bytes | +| `M[0..16]` | 16 words | 64 | input message bytes | +| `T_LO,T_HI,BLEN,FLAGS` | 4 words | 16 | counter split, block_len, flags | +| per-G logic × 48 G | see §5 | 2,976 | add/xor/shift SSA words + carry bits | +| feed-forward `OUT[0..16]` | 16 words | 64 | XOR outputs | +| **main columns total** | | **≈ 3,155** | | +| aux (LogUp) `= 3·⌈1250/2⌉` | | **1,875** | degree-3 ext columns | + +Per-G committed breakdown (each of the 48 G-instances): + +| sub-op | SSA output | bytes | carry bits | +|---|---|---:|---:| +| `add3` v[a]=v[a]+v[b]+mx | `A1` | 4 | 2 | +| `xor` v[d]^v[a] (→rotr16 free) | `X1` | 4 | – | +| `add2` v[c]+v[d] | `C1` | 4 | 1 | +| `xor` v[b]^v[c] | `X2` | 4 | – | +| `rotr12`(X2) | `SLLlo,SLLClo,SLLhi,SLLChi,B1` | 12 | – | +| `add3` v[a]=v[a]+v[b]+my | `A2` | 4 | 2 | +| `xor` v[d]^v[a] (→rotr8 free) | `X3` | 4 | – | +| `add2` v[c]+v[d] | `C2` | 4 | 1 | +| `xor` v[b]^v[c] | `X4` | 4 | – | +| `rotr7`(X4) | `SLLlo,SLLClo,SLLhi,SLLChi,B2` | 12 | – | +| **per G** | | **56** | **6** | + +`rotr16`/`rotr8` produce **no columns** — the next consumer reads the XOR-output +bytes in relabeled order (see §4.2). + +--- + +## 4. Constraints & bus interactions + +All arithmetic reduces to the existing precomputed-BITWISE receivers +(`prover/src/tables/bitwise.rs`); all eval constraints are **μ-gated**, so +degree = (μ:1) × (body). Padding rows are all-zero and μ=0. + +### 4.1 XOR — `ByteAlu[XOR]` send (per byte) + +For each 32-bit XOR, 4 sends `ByteAlu[XOR, a_byte, b_byte] → out_byte` +(`bitwise.rs:903`). The lookup **simultaneously** byte-range-checks both operands +and pins `out` to the exact XOR — no separate range check. Operands may be linear +combos (the byte contract requires `sum ≤ 255`), which lets a free rotation be +read in-place. Eval constraints: none (pure lookup). Degree: n/a. + +### 4.2 Rotations + +* **rotr16 / rotr8 — free.** rotr16 = byte relabel `[b0,b1,b2,b3]→[b2,b3,b0,b1]`; + rotr8 = `[b1,b2,b3,b0]` (oracle §3.1, exhaustively verified). No columns, no + lookups, no constraints — the consumer indexes the source XOR's bytes in + rotated order. +* **rotr12 / rotr7 — inline shift identity (chosen over HWSL sends).** + `rotr12 = rotl20 = rotl16∘rotl4` (inner `r=4`); `rotr7 = rotl25 = rotl16∘rotl9` + (`r=9`). For input word `X = xlo + 2^16·xhi` (halfwords `xlo,xhi`, 2 bytes each): + + **Shift identities (eval, degree 2 after ×μ):** + ``` + μ·( xlo·2^r − SLLC_lo·2^16 − SLL_lo ) = 0 + μ·( xhi·2^r − SLLC_hi·2^16 − SLL_hi ) = 0 + ``` + **Recombine + halfword swap (eval, degree 2 after ×μ):** + ``` + μ·( Ylo − SLL_hi − SLLC_lo ) = 0 # output low halfword = Y[0]+256·Y[1] + μ·( Yhi − SLL_lo − SLLC_hi ) = 0 # output high halfword = Y[2]+256·Y[3] + ``` + **Range checks (sends):** `AreBytes` on the 8 bytes of `SLL_lo,SLLC_lo,SLL_hi, + SLLC_hi` = 4 sends/rotation (`bitwise.rs:783`). `Y` is range-checked *free* by + the downstream XOR that consumes it. + + Soundness (originally deferred to `../keccak-verify/hwsl_inline_test.py` + Part 2 — **that file is lost, see the banner at the top; the result was + re-derived independently and more strongly by the 2026-07-29 audits** — and by + the width audit in the gate): given `SLL_* ∈ [0,2^16)` (the tight remainder bound + from AreBytes) and `2^16` invertible mod p, the identity **uniquely** pins + `SLL = (xlo·2^r) mod 2^16` and `SLLC = (xlo·2^r) >> 16`; the loose 16-bit bound + on `SLLC` suffices because it is the quotient, not the remainder. The two + recombination sums are over non-overlapping bit ranges, so `+` = `OR` and each + is an exact 16-bit halfword. + + **Refined by the transcription audits (2026-07-29), symbolically over all + 2^32 inputs — the earlier wording was coarser than the truth:** + * The load-bearing bound set is **at least one of `{SLL_lo, SLL_hi}`**. Every + configuration with neither is forgeable; every configuration with either is + pinned. **The two `SLLC` bounds are not load-bearing at all** — so of the 4 + `AreBytes` sends per rotation, only the `SLL` pair carries soundness weight. + Read the sentence above as "a tight bound on at least one `SLL` halfword", + not "the tight `SLL` bound". + * The *composed* forgery (both `SLL` bounds dropped) exists for exactly **one** + input, `X = 0xFFFFFFFF` → forged `Y = 0`, exhaustively confirmed for both + `r = 4` and `r = 9`. The gate's isolated control makes it look reachable for + arbitrary inputs; it is not. Narrow, but a forgery is a forgery. + * The rotation **output** needs no range check of its own: the two recombine + identities pin its value even with free field cells. So the §4.7 "free range + check" argument is load-bearing for the **add** outputs and one `SLL` + halfword per rotation — not for the rotation output. + + **HWSL alternative, priced:** replace each shift identity with an `Hwsl` send + (`bitwise.rs:831`). Cost/rotation: +2 Hwsl sends, same AreBytes, same columns. + Per compression that is +4 sends/G × 48 = +192 sends → +288 aux cells (≈6%). + Inline wins because the eval identity is free of columns/sends; it costs only + degree budget (2 ≤ 3). **Use inline.** + +### 4.3 Two-operand add — `emit_add_pair` low half (eval, degree 3 after ×μ) + +`s = (a+b) mod 2^32`; one carry bit. Following `templates.rs:334`: +``` +carry = (a + b − s)·2^-32 # linear expression, INV_SHIFT_32 = (2^32)^-1 +μ · carry·(1 − carry) = 0 # degree (1)×(1)×(1 body)=2, ×μ = 3 +``` +`s`'s bytes are range-checked **free** by the next XOR that consumes `s` +(every add output in G feeds a subsequent XOR — see §5). Booleanity + `s∈[0,2^32)` +⇒ `s` unique. + +### 4.4 Three-operand add — **O1 resolved: option (c), two summed carry bits** + +`s = (a+b+m) mod 2^32`, carry ∈ {0,1,2}. Commit two carry **bits** `c1,c2` +(2 cells, no intermediate word): +``` +μ·( a + b + m − s − 2^32·(c1+c2) ) = 0 # sum identity, linear → ×μ = degree 2 +μ · c1·(1 − c1) = 0 # ×μ = degree 3 +μ · c2·(1 − c2) = 0 # ×μ = degree 3 +``` +`s`'s bytes range-checked free downstream. `c1+c2 ∈ {0,1,2}` covers the carry; +`s∈[0,2^32)` + the sum identity pin `s = (a+b+m) mod 2^32` uniquely (proof in the +gate's width audit). + +**Why (c):** + +| O1 option | extra committed / 3-op add | degree (ungated → ×μ) | legal under ×μ? | +|---|---|---|---| +| (a) one ternary carry `k(k−1)(k−2)=0` | 1 bit | 3 → **4** | ❌ (μ-gating mandatory, §4.5) | +| (b) two chained binary adds | 1 word (4 B) + 2 AreBytes | 2 → 3 | ✅ but +4B +2 sends | +| **(c) two summed carry bits** | **2 bits** | 2 (bool) / 1 (sum) → 3 / 2 | ✅ **cheapest** | + +Over a compression, (c) vs (b): saves (4B−2bit) per 3-op add × 96 three-op adds +≈ **300 committed cells + 192 AreBytes sends**. (c) is a strict refinement of the +oracle's two options. + +### 4.5 μ-gating & padding — **O2 resolved: gate everything, all-zero padding** + +Every eval constraint is multiplied by `μ` (the `MU` column, 1 on the real row, +0 on padding), exactly like `keccak_rnd`'s IS_BIT (`keccak_rnd.rs:914`). Padding +rows are **all-zero**: +* bus interactions carry `Multiplicity::Column(MU)` ⇒ 0 contribution on padding; +* eval constraints are `μ·(…)` ⇒ 0 on padding regardless of the (zero) cells. + +This is why O1 must be (b) or (c): the ternary carry (a) is degree 3 *ungated*, +and ×μ pushes it to 4. Inlined `IV` constants are fine because the round-0 add +that consumes them is itself μ-gated (its carry expression is nonsense on an +all-zero padding row, but ×μ=0 kills it). **The μ-gating requirement is what +forecloses option (a) — this is the single tightest coupling in the design.** + +### 4.6 Feed-forward (16 XORs, all `ByteAlu[XOR]`) + +``` +out[i] = v[i] ⊕ v[i+8] i = 0..8 (v[i+8] = final state word) +out[i+8] = v[i+8] ⊕ h[i] i = 0..8 (h = original input CV column) +``` +64 sends, 64 committed output bytes (the XOR outputs), range-checked free by the +lookup. Output bytes are shipped on the `Blake3` send. + +### 4.7 Range checks that are NOT free + +The message `m` enters **only** through adds (never XORed), so its 64 bytes need +explicit `AreBytes` (32 sends/compression). `h` and `t/block_len/flags` all feed +an XOR (feed-forward / round-0 diagonal), so they are free. Every add/shift/xor +output feeds a downstream XOR ⇒ free. + +### 4.8 Degree ledger (the hard gate) + +| constraint | body degree | × μ | ≤ 3? | +|---|---:|---:|:--:| +| 2-op add carry booleanity | 2 | 3 | ✅ | +| 3-op add sum identity | 1 | 2 | ✅ | +| 3-op add carry booleanity ×2 | 2 | 3 | ✅ | +| shift identity (×2) | 1 | 2 | ✅ | +| recombine identity (×2) | 1 (was stated as 2) | 2 | ✅ | +| (rejected) ternary carry | 3 | **4** | ❌ | + +Worst legal constraint = 3. **No constraint exceeds 3.** + +--- + +## 5. Per-G dataflow, SSA + free range-checks + +``` +A1 = add3(v[a], v[b], mx) # v[a] ; 2 carry bits ; range-checked by X1 +X1 = xor(v[d], A1) ; v[d] = rotr16(X1) # free relabel +C1 = add2(v[c], v[d]=rotr16(X1)) # v[c] ; 1 carry bit ; range-checked by X2 +X2 = xor(v[b], C1) +B1 = rotr12(X2) # v[b] ; range-checked by X4 / next round +A2 = add3(A1, B1, my) # v[a] ; 2 carry bits ; range-checked by X3 +X3 = xor(v[d]=rotr16(X1), A2) ; v[d]=rotr8(X3) +C2 = add2(C1, v[d]=rotr8(X3)) # v[c] ; 1 carry bit ; range-checked by X4 +X4 = xor(B1, C2) +B2 = rotr7(X4) # v[b] ; range-checked next round / FF +``` +Every committed add/shift word is an operand of a later XOR ⇒ its bytes are +byte-range-checked for free by that `ByteAlu` lookup. Confirmed: no add/shift +output needs its own AreBytes. (Only `m` does — §4.7.) + +--- + +## 6. Cost & comparison + +| quantity (6-round) | value | +|---|---:| +| committed main columns | ≈ 3,150 | +| bus sends N | ≈ 1,250 (832 XOR incl. 64 feed-forward + 384 shift-AreBytes + 32 msg-AreBytes + 2 I/O) | +| aux base cells (3·⌈N/2⌉) | ≈ 1,875 | +| **total cell-equiv / compression** | **≈ 5,030** | +| 7-round variant | ≈ 5,810 | +| keccak-f permutation (reference) | ≈ 77,000 | +| **BLAKE3-6r as fraction of keccak-f** | **≈ 1/15 (6.5%)** | + +Dominated by the ~960 byte-XOR lookups, as the oracle predicted. Note: the +oracle's prose "¼–⅓ of a keccak permutation" is inconsistent with its own +5–6k/compression figure; the detailed count here (≈5k vs 77k) puts it at **~1/15**. + +--- + +## 7. Soundness-critical spots a Rust implementation must NOT deviate from + +1. **μ-gate every eval constraint** (carry booleanity, sum identity, shift + identity, recombine). Un-gated ternary carry or an un-gated constraint with + inlined IV constants breaks all-zero padding. (§4.5) +2. **3-op add = two summed carry bits with the explicit sum identity** — not a + single ternary carry (degree 4 after gating), and the sum identity must be + present (without it, `s` is only constrained mod nothing). (§4.4) +3. **Shift identity needs a tight `∈ [0,2^16)` AreBytes bound on at least one of + `SLL_lo`/`SLL_hi`** (the `SLLC` bounds are *not* load-bearing — audited + 2026-07-29, §4.2); dropping it + makes the rotation forgeable (a wrong `SLL` admits a large field `SLLC`). + Soundness relies on `2^16` invertible mod p — a BV model cannot see this; + verify in the field (gate width audit + `hwsl_inline_test.py`). (§4.2) +4. **Every add output must actually feed a downstream XOR** (its only range + check). If a future refactor reorders so an add output is *last* with no XOR + consumer, add an explicit AreBytes or the carry argument is unsound. (§5) + + ⚠ **THE GATE CANNOT CHECK THIS, and both 2026-07-29 audits confirmed it with + explicit forgeries.** `build_g` returns each add output as `fresh_word()` = + 4×`BitVec(...,8)`, so byte range is **declared by construction, never derived + from a modelled lookup**. The gate therefore proves the identical UNSAT for a + chip that has the downstream XOR and for one that does not. Drop it and the + sum is forgeable — witness `a = b = 0x80000000`, honest `s = 0`, forged + `s = 2^32` with `carry = 0`, satisfying every modelled constraint. This + invariant rests entirely on the implementer, and a green board is not + evidence for it. +5. **Message `m` needs explicit AreBytes** — it is never XORed. (§4.7) + ⚠ Same blind spot: the gate declares `m` as 16×4 `BitVec(...,8)`, so it proves + the same UNSAT with or without those 32 `AreBytes` sends. Without them a + message word has many cell representations of one value over `F_p` (e.g. + `[0x9A,0,0x13,0x7F]` and `[0x19A,p−1,0x13,0x7F]`), because the chip binds + `Σ m_i·2^(8i)`, not the 64 bytes. +6. **rotr16/rotr8 byte order** exactly `[b2,b3,b0,b1]` / `[b1,b2,b3,b0]` + (little-endian). A wrong relabel silently corrupts. (§4.2) +7. **Message permutation `permute^r`** wired per round from the *original* 16 + `M` columns; MSG_PERMUTATION = `[2,6,3,10,7,0,4,13,1,11,12,5,9,14,15,8]`. The + trailing permute after the last round is unused (oracle §2.4). (Gate control + `wrong_msg_index`.) +8. **IV / feed-forward / counter split** exactly per oracle §2.4: + `v[8..12]=IV[0..4]` inlined, `v[12]=t_lo, v[13]=t_hi, v[14]=block_len, + v[15]=flags`; `out[i]=v[i]⊕v[i+8]`, `out[i+8]=v[i+8]⊕h[i]`. (Controls + `wrong_iv`, `drop_ff_xor`.) +9. **Non-overflow side conditions (width audit):** all add/shift field + expressions stay `< 2^35 ≪ p`, so `≡0 mod p` ⇒ `=0` as integers; the whole + soundness argument depends on operands being genuine ≤32-bit (byte columns) + and carries being genuine bits. +10. **`TIMESTAMP_0/1` in BOTH the `Blake3` receive and send** (§1.1). Without a + key in both tuples nothing binds a row's inputs to its own outputs, and two + compressions can swap results while the bus still balances. `keccak.rs` does + this correctly and is the pattern to copy. **The gate cannot catch a + violation** — it models arithmetic only, with no bus layer — so this one is + on the implementer, not on a green board. +11. **Every G instance must be wired as MAIN 0 models it.** MAIN 0 proves *one* + G under free inputs; the 48 unrolled instances are emitted separately, so a + wrong column or message index in a single instance is invisible to it. The + concrete positive controls are what cover that — keep them runnable, and run + `--full`'s monolithic UNSAT before shipping Rust. + +--- + +## 8. Gate + +`z3_blake_verify.py` — free-variable model of every column, every lookup/eval +constraint as an equation, `assert output ≠ oracle-reference`, ask z3 for a +counterexample. Reference (`bref_*`) is an independent 32-bit-BV port of +`blake3_ref.py` (RotateRight / + / ^), structurally independent of the byte-level +shift wiring. Results are appended to §9 after the run (`run.log`). +``` +python3 z3_blake_verify.py # round + wrapper + controls + audit (fast) +python3 z3_blake_verify.py --full # + monolithic 6- and 7-round UNSAT +``` + +## 9. Gate results + +Default run (`python3 z3_blake_verify.py`, ~2 min) — **OVERALL: PASS**: + +| check | result | meaning | +|---|---|---| +| **MAIN 0** — one G-function, free inputs | **UNSAT** | the quarter-round (byte-XOR + inline rotr12/rotr7 shift identities + 2-op & 3-op adds) is correctly & tightly constrained; **covers every G, hence every round** (a round is a fixed composition of 8 G-calls). | +| **MAIN 1** — init-state + feed-forward (rounds=0) | **UNSAT** | `v` layout (`h`/IV/counter-split/block_len/flags) and `out[i]=v[i]⊕v[i+8]`, `out[i+8]=v[i+8]⊕h[i]` are correct. | +| neg `rot_wrong_amount` | **SAT** | wrong rotation amount detected. | +| neg `swap_g_operand` | **SAT** | swapped G-function operand detected. | +| neg `wrong_iv` | **SAT** | wrong IV constant detected. | +| neg `drop_ff_xor` | **SAT** | dropped feed-forward XOR detected. | +| neg `wrong_msg_index` | **SAT** | wrong message-schedule index detected (permutation is load-bearing). | +| **pos** 6-round seeds 0,1,2 (canonical vectors) | **SAT** | full 6-round pipeline reproduces the oracle's recorded output for concrete inputs. | +| **pos** 7-round (oracle-generated) | **SAT** | full 7-round pipeline reproduces the oracle's `compress(…,rounds=7)`. | +| audit: shift `SLL` 16-bit bound present | **UNSAT** | with AreBytes the shift output is pinned. | +| audit: **DROP `SLL` bound** (field neg ctrl) | **SAT** | without it the rotation is forgeable (needs `2^16` invertible mod p). | +| audit: 3-add carry booleanity present | **UNSAT** | with booleanity the sum `s` is pinned. | +| audit: **DROP carry booleanity** (field neg ctrl #4) | **SAT** | without it `s` is forgeable in the prime field. | + +**The 6th team-lead control — "dropped carry booleanity" — lives in the width +audit, not the BV controls, and this is correct.** Dropping a committed carry +column's booleanity is a *field-level* soundness bug: the column becomes a full +Goldilocks element, but a *bounded-BV* model keeps the 8-bit carry + `s∈[0,2^32)` +byte-range, which still pins `s`, so BV reports UNSAT (verified: the BV version +does). Only the mod-p model exhibits the forgery — exactly the phenomenon +`../keccak-verify/hwsl_inline_test.py` Part 2 documents (`2^16`/`2^32` are zero +divisors mod `2^n`). The gate deliberately separates BV-observable logic bugs +from field-only soundness bugs; both classes fire. + +**`--full`** additionally runs the heavy monolithic symbolic UNSATs (one round; +compression rounds=2 for the permutation; full 6- and 7-round). These are *bonus* +confirmations — the G-unsat + fixed-composition chaining argument + rounds=0 + +the concrete full-pipeline positive controls already establish full-compression +correctness. (The direct 6-round symbolic UNSAT is large; it is not required for +the verdict and may take a long time / be run offline.) + +### What is and isn't proven +* **Proven (symbolic, all inputs):** the G quarter-round; the init-state layout; + the feed-forward — hence, by the chaining argument, the full N-round + compression for **both ROUNDS=6 and ROUNDS=7**. +* **Proven (concrete, external anchor):** the *entire* unrolled pipeline + (init + 6/7 rounds + message permutation + feed-forward) reproduces the + oracle's validated vectors. +* **Proven (field-level):** the AreBytes shift bound and the add-carry booleanity + are each *necessary* (dropping either is a forgery mod p). +* **Assumed (assume-guarantee, not re-proven here):** the precomputed BITWISE + table contracts themselves (ByteAlu[XOR], AreBytes) — these are existing, + separately-audited chips (`prover/src/tables/bitwise.rs`). Same assumption the + keccak gate makes. +* **Not modeled here:** the memory/syscall I/O variant (§1.2) — orthogonal; + open questions O5 (counter width, already covered by the Plonky3 anchor) and + O6 (endianness at the MEMW boundary) live there and must be pinned when that + interface is wired. diff --git a/thoughts/blake3/blake3-chip/IMPLEMENTATION.md b/thoughts/blake3/blake3-chip/IMPLEMENTATION.md new file mode 100644 index 000000000..fda2cd634 --- /dev/null +++ b/thoughts/blake3/blake3-chip/IMPLEMENTATION.md @@ -0,0 +1,138 @@ +# BLAKE3 chip — implementation notes (syscall variant) + +Companion to `DESIGN.md`: what the Rust implementation +(`prover/src/tables/blake3.rs` + the executor syscall) does differently from +the internal-variant design, and why. The design's §7 soundness ledger is +reproduced in the chip's module docs with per-item dispositions. + +## Variant + +DESIGN.md §1.1 designs the **lean internal interface** (a `Blake3` bus with a +parent-node caller). No in-circuit caller exists yet, so what is built is the +**§1.2 general syscall variant**: `Ecall` receiver + `Memw` register read of +x10 + per-dword `Memw` I/O, copied idiom-for-idiom from `keccak.rs`. The +internal bus — and with it §7 item 10 (TIMESTAMP binding in both bus tuples) — +does not exist in this variant: a row's inputs and outputs are tied by being +committed on the same row. + +ABI: `x10` → 8-aligned 176-byte region, `h[32] | m[64] | t[8] | +block_len,flags[8] | out[64]` (see `BLAKE3_SYSCALL_NUMBER` docs). Syscall +number `u64::MAX - 2`. + +## Deltas from the design's cell accounting + +| item | DESIGN §3 | implemented | why | +|---|---|---|---| +| add2 carries | 1 committed bit each (§3 table; 6 bits/G) | **expression carry, no cell** (4 bits/G) | §4.3's own formula is the `emit_add_pair` expression form; the §3 table double-counts it. Saves 96 cells/row. | +| G block | 62 cells | **60 cells** (56 bytes + 4 bits) | above | +| I/O apparatus | none (internal variant) | +8 addr bytes, +88 ptr halfwords, +64 OLD_OUT | syscall variant | +| OLD_OUT | n/a | 64 committed bytes + 32 AreBytes | the 8 out-dword `Memw` ops need the previous memory content in their `old` field; those bytes ride only the Memw bus, so they get explicit byte checks (same aliasing argument as keccak.rs's addr bytes) | +| columns | ≈3,155 | **3,219** | | +| sends | ≈1,250 | **1,397** (832 XOR + 384 shift-AreBytes + 32 m + 32 old_out + 4 addr + 1 AND + 88 IS_HALF + 24 I/O) | | +| aux (3·⌈N/2⌉) | ≈1,875 | **2,097** | | +| **cell-equiv/compression** | ≈5,030 | **≈5,316** | +5.7% for the syscall I/O | + +Against keccak-f post-#889 (72,672 cell-equiv): **≈ 1/13.7 per call**, ~6.4× +per byte (64 B vs 136 B absorbed). + +## The single-dataflow rule + +The compression dataflow exists once (`run_flow`), interpreted twice: +`WireFlow` (columns → constraints + bus senders) and `ValueFlow` (u32 → +trace filling + BITWISE multiplicities). Wiring divergence between prover +cells, senders and receiver multiplicities is therefore impossible by +construction; only interpretation bugs remain, and those are what the oracle +vectors + the e2e bus-balance gate check. + +## Gates run + +- executor ↔ oracle: the 10 pinned canonical 6-round vectors + (`canonical_6round_vectors.json`, full-width `t` values — the counter-split + order is load-bearing) + syscall-level tests (alignment/overflow rejection, + input-region non-clobbering). +- `ValueFlow` ↔ executor: differential unit test. +- wire audit: every committed mixing cell written exactly once, in-range + (unit test `wire_flow_counts`). +- e2e: `test_prove_elfs_blake3` — two chained compressions (the second + consumes the first's output and overwrites a non-zero out region), prove + + verify, which exercises bus balance across Ecall/Memw/ByteAlu/AreBytes/ + IsHalfword. +- the z3 gate (`z3_blake_verify.py`) proves the *design*; the transcription + design → Rust is covered by the vectors + e2e, per the gate's own + documentation of what it cannot see (§7 items 4, 5, 11). +- **`--full` monolithic UNSATs, run 2026-08-06: ATTEMPTED-INCONCLUSIVE, not + satisfied.** `z3_blake_verify.py --full` ran ~145 min and exited 1 + (`OVERALL: FAIL`). All four monolithic queries hit z3's resource limit: + + ``` + round (clean) -> unknown (want unsat) + compress rounds=2 -> unknown (want unsat) + compress rounds=6 -> unknown (want unsat) + compress rounds=7 -> unknown (want unsat) + ``` + + `unknown` is the timeout return — the checks `s.set("timeout", timeout_ms)` + then `return s.check()` (`z3_blake_verify.py:320-321`, `:340-341`), and the + verdict tests `== unsat` (line 553), so a timeout scores `False` and drags + OVERALL to FAIL. Timing corroborates a clean sweep of timeouts: the budgets + are 30+30+40+40 = 140 min against ~145 min wall. **No counterexample was + found — nothing was disproven — but no monolithic UNSAT was obtained + either.** The fast board is unchanged and fully green: + + ``` + G-function UNSAT (covers all G) : True + init+feed-forward UNSAT (rounds=0): True + negative controls all SAT : True + positive controls all SAT : True (full 6-/7-round pipeline, concrete) + ``` + + DESIGN.md §7 item 11's "run `--full`'s monolithic UNSAT before shipping + Rust" precondition is therefore **attempted but not satisfied**; the + coverage of the 48 unrolled G instances still rests on the concrete + positive controls plus the per-instance index mutant, which do pass. + Remediation: rerun with a much larger timeout budget on a server (the run + is single-threaded and CPU-bound), and/or restructure the monolithic query + as round-by-round induction instead of one flat bit-vector problem. + +## Known costs and open items + +- **Always-on AIR**: `FIXED_TABLE_COUNT` 10 → 11. Every proof now carries a + ≥4-row BLAKE3 table (~3.2k cols) even when unused. This is exactly the + EC-campaign regression shape (PR #871, +3 near-empty AIRs → +25%); one + near-empty table is far smaller, but a real-block ABBA is REQUIRED before + merge. +- The proof wire format changes (one more sub-proof); old proofs do not + verify against this branch. The recursion guest would need a rebuild + (the in-repo recursion PoC is already non-functional, see project notes). +- `count_table_lengths` (disk-spill sizing) does not count the 23 Memw ops a + blake3 ecall contributes; disk-spill runs of blake3-heavy workloads would + size MEMW slightly small. Not exercised by the bench (no disk-spill). +- 7-round variant: `BLAKE3_ROUNDS` is the single knob; columns/constraints/ + sends all derive from it. Standard-BLAKE3 compatibility would also need the + flags/t plumbed per the tree mode (out of scope here). + +## The 6-round assumption (sign-off record) + +The chip implements 6 rounds, not the standard 7. The z3 gate proves the chip +matches the 6-round reference; it does not and cannot prove 6 rounds are +collision-resistant. Adopting this for Merkle/Fiat–Shamir rests on the named +assumption: + +> **A6R**: the BLAKE3 compression function restricted to 6 rounds is +> collision-resistant and suitable as a 2-to-1 compression for Merkle +> hashing and as a PRF for Fiat–Shamir, in the same sense the full 7-round +> function is believed to be (precedent: KangarooTwelve's reduced-round +> Keccak). + +Directed for implementation by the project owner, 2026-08-05 ("trust me" +sign-off in session). Recorded in the spec (`spec/blake3.typ`, A6R section). + +**External review (2026-08-06, relayed by the project owner):** the round +count was reviewed with external symmetric-cryptography experts — removing +one round (7→6) judged comfortable, removing two (7→5) explicitly not. +6 rounds is therefore the endorsed floor. Sub-6 variants are not formally +dead, but they cannot be adopted on the project's own authority — that +would need the experts to sit with the reduced margin specifically +(dedicated cryptanalytic review, not an engineering call). The 7-round +instantiation remains available as the zero-assumption / interop fallback +at ~10-12% more per merge. diff --git a/thoughts/blake3/blake3-chip/z3_blake_verify.py b/thoughts/blake3/blake3-chip/z3_blake_verify.py new file mode 100644 index 000000000..f6a439f98 --- /dev/null +++ b/thoughts/blake3/blake3-chip/z3_blake_verify.py @@ -0,0 +1,561 @@ +""" +Formal (z3 / QF_BV) assume-guarantee gate for the BLAKE3 compression chip design. + +Method (mirrors ../keccak-verify/z3_verify.py): + * Every committed column of the designed chip is a FREE bitvector. + * Every bus lookup (under its precomputed-table contract) and every eval + constraint becomes an equation relating those free vars. + * The chip OUTPUT is whatever the constraints force. We assert + `output != reference(input)` and ask z3 for a counterexample: + UNSAT -> for all constraint-satisfying assignments, output == reference + (the chip is correctly & tightly constrained). + SAT -> the constraints permit a wrong output (under-constrained / mis-wired). + +The reference (`bref_*`) is written directly from the BLAKE3 spec with 32-bit +BV ops (RotateRight / + / ^) — structurally INDEPENDENT of the chip's byte-level +XOR / halfword-shift wiring, exactly like keccak's zref_round vs the byte circuit. + +Chip contracts assumed (assume-guarantee, from prover/src/tables/bitwise.rs): + ByteAlu[XOR](a,b)->c : a,b,c are bytes and c = a ^ b. (8-bit width = byte + range-check; output pinned by the precomputed table.) + AreBytes[a,b] : a,b are bytes (8-bit width). + (HWSL is NOT used: rotations are inlined as the mu-gated linear shift identity + in*2^r == SLLC*2^16 + SLL, whose soundness is proven by ../keccak-verify/ + hwsl_inline_test.py given the AreBytes 16-bit bounds + 2^16 invertible mod p.) + +Add carries and shift decompositions are eval constraints (mu-gated, degree <=3); +here mu=1 (a real row), so mu drops out and we model the ungated equation. + +DESIGN DECISIONS UNDER TEST (see DESIGN.md): + * State stored as bytes; XOR byte-wise via ByteAlu[XOR]. + * rotr16 / rotr8 : FREE byte relabels (no columns, no lookups). + * rotr12 / rotr7 : inner rotl r=4 / r=9 -> two halfword shift-identities + + cross-halfword recombine + halfword swap. + * 2-operand add : one carry bit, a+b == s + 2^32*carry, s range-checked. + * 3-operand add : O1 option (c) -- TWO summed carry bits c1,c2 in {0,1}, + a+b+m == s + 2^32*(c1+c2). (No committed intermediate word; + degree stays <=3 after mu-gating, unlike k(k-1)(k-2).) +""" +import sys +import json +import os +from z3 import ( + BitVec, BitVecVal, Concat, ZeroExt, RotateRight, Or, And, Solver, sat, unsat, + Int, IntVal, +) + +# --------------------------------------------------------------------------- +# BLAKE3 constants (spec; cross-checked against Plonky3 in the oracle) +# --------------------------------------------------------------------------- +IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] +MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] +MASK32 = 0xFFFFFFFF +WIDE = 48 # wide BV width for add / shift identities (honest < 2^35 << 2^48) +P = 2**64 - 2**32 + 1 # Goldilocks prime (used in the width-audit field checks) + +# G-function schedule: (a,b,c,d, mx_index, my_index) for the 8 calls of a round. +G_CALLS = [ + (0, 4, 8, 12, 0, 1), + (1, 5, 9, 13, 2, 3), + (2, 6, 10, 14, 4, 5), + (3, 7, 11, 15, 6, 7), + (0, 5, 10, 15, 8, 9), + (1, 6, 11, 12, 10, 11), + (2, 7, 8, 13, 12, 13), + (3, 4, 9, 14, 14, 15), +] + +# =========================================================================== +# Independent z3-native reference (BLAKE3 spec, 32-bit BV words) +# =========================================================================== +def bref_g(v, a, b, c, d, mx, my): + v[a] = v[a] + v[b] + mx + v[d] = RotateRight(v[d] ^ v[a], 16) + v[c] = v[c] + v[d] + v[b] = RotateRight(v[b] ^ v[c], 12) + v[a] = v[a] + v[b] + my + v[d] = RotateRight(v[d] ^ v[a], 8) + v[c] = v[c] + v[d] + v[b] = RotateRight(v[b] ^ v[c], 7) + + +def bref_round(v, m): + for (a, b, c, d, ix, iy) in G_CALLS: + bref_g(v, a, b, c, d, m[ix], m[iy]) + + +def bref_permute(m): + return [m[MSG_PERMUTATION[i]] for i in range(16)] + + +def bref_round_only(state16, msg16): + """One round, free 16-word state + free 16-word message -> new state.""" + v = list(state16) + bref_round(v, msg16) + return v + + +def bref_compress(h, m, tlo, thi, bl, fl, rounds): + """Full compression. h:8 BV32, m:16 BV32, counter split tlo/thi, bl, fl.""" + v = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], + BitVecVal(IV[0], 32), BitVecVal(IV[1], 32), + BitVecVal(IV[2], 32), BitVecVal(IV[3], 32), + tlo, thi, bl, fl] + schedule = list(m) + for r in range(rounds): + bref_round(v, schedule) + if r < rounds - 1: + schedule = bref_permute(schedule) + out = [None] * 16 + for i in range(8): + out[i] = v[i] ^ v[i + 8] + out[i + 8] = v[i + 8] ^ h[i] + return out + + +# =========================================================================== +# Chip circuit model. A "word" is a list of 4 free 8-bit BVs [b0,b1,b2,b3] +# (little-endian). Byte width == the ByteAlu/AreBytes range-check contract. +# =========================================================================== +class Circuit: + def __init__(self, tag, bug=None): + self.C = [] + self.tag = tag + self.bug = bug + self.n = 0 + + def _fresh(self, w=8): + v = BitVec(f"{self.tag}_v{self.n}", w) + self.n += 1 + return v + + def fresh_word(self): + return [self._fresh(8) for _ in range(4)] + + def const_word(self, val): + return [BitVecVal((val >> (8 * i)) & 0xFF, 8) for i in range(4)] + + # -- value helpers ----------------------------------------------------- + def wval(self, word): + """word as a WIDE-bit BV integer (little-endian byte combination).""" + acc = BitVecVal(0, WIDE) + for i in range(4): + acc = acc + ZeroExt(WIDE - 8, word[i]) * BitVecVal(1 << (8 * i), WIDE) + return acc + + def hwval(self, blo, bhi): + """halfword (2 bytes) as a WIDE-bit BV.""" + return ZeroExt(WIDE - 8, blo) + ZeroExt(WIDE - 8, bhi) * BitVecVal(256, WIDE) + + def word32(self, word): + return Concat(word[3], word[2], word[1], word[0]) + + def fresh_bit(self, boolean=True): + v = self._fresh(8) + if boolean: + self.C.append(Or(v == 0, v == 1)) # mu-gated IS_BIT (mu=1 here) + return v + + # -- operations under contract ---------------------------------------- + def xor(self, A, B): + """ByteAlu[XOR]: out byte-wise = A ^ B (auto byte range-check).""" + out = self.fresh_word() + for i in range(4): + self.C.append(out[i] == A[i] ^ B[i]) + return out + + def rotr16(self, A): + # rotate-right 16 == swap halfwords == byte relabel [b2,b3,b0,b1]. FREE. + return [A[2], A[3], A[0], A[1]] + + def rotr8(self, A): + # rotate-right 8 == byte relabel [b1,b2,b3,b0]. FREE. + return [A[1], A[2], A[3], A[0]] + + def add2(self, A, B, drop_bool=False): + """2-operand add mod 2^32: a+b == s + 2^32*carry, carry in {0,1}.""" + s = self.fresh_word() + carry = self.fresh_bit(boolean=not drop_bool) + self.C.append( + self.wval(A) + self.wval(B) + == self.wval(s) + ZeroExt(WIDE - 8, carry) * BitVecVal(1 << 32, WIDE) + ) + return s + + def add3(self, A, B, M, drop_bool=False): + """3-operand add mod 2^32 (O1 option c): TWO summed carry bits. + a+b+m == s + 2^32*(c1+c2), c1,c2 in {0,1}.""" + s = self.fresh_word() + c1 = self.fresh_bit(boolean=not drop_bool) + c2 = self.fresh_bit(boolean=not drop_bool) + csum = ZeroExt(WIDE - 8, c1) + ZeroExt(WIDE - 8, c2) + self.C.append( + self.wval(A) + self.wval(B) + self.wval(M) + == self.wval(s) + csum * BitVecVal(1 << 32, WIDE) + ) + return s + + def rotr(self, A, n, wrong_amount=False): + """rotr12 / rotr7 via inner rotl r + halfword swap. + + r=4 for n=12 (rotl20=rotl16.rotl4); r=9 for n=7 (rotl25=rotl16.rotl9). + Shift identity (inline, mu-gated): hw*2^r == SLLC*2^16 + SLL, with SLL + the tight 16-bit remainder and SLLC the (loose 16-bit) quotient. Then + Y_lo = SLL_hi + SLLC_lo, Y_hi = SLL_lo + SLLC_hi (non-overlapping adds). + """ + r = {12: 4, 7: 9}[n] + if wrong_amount: + r += 1 # negative control: wrong rotation amount + xlo = self.hwval(A[0], A[1]) + xhi = self.hwval(A[2], A[3]) + # SLL / SLLC as free halfwords (each = 2 free bytes -> AreBytes 16-bit). + sll_lo = self.fresh_word()[:2] + sllc_lo = self.fresh_word()[:2] + sll_hi = self.fresh_word()[:2] + sllc_hi = self.fresh_word()[:2] + SLL_lo, SLLC_lo = self.hwval(*sll_lo), self.hwval(*sllc_lo) + SLL_hi, SLLC_hi = self.hwval(*sll_hi), self.hwval(*sllc_hi) + two_r = BitVecVal(1 << r, WIDE) + two_16 = BitVecVal(1 << 16, WIDE) + # shift identities + self.C.append(xlo * two_r == SLLC_lo * two_16 + SLL_lo) + self.C.append(xhi * two_r == SLLC_hi * two_16 + SLL_hi) + # recombine (rotl_r) + halfword swap (rotl16) + Y = self.fresh_word() + self.C.append(self.hwval(Y[0], Y[1]) == SLL_hi + SLLC_lo) # Y low halfword + self.C.append(self.hwval(Y[2], Y[3]) == SLL_lo + SLLC_hi) # Y high halfword + return Y + + +# --------------------------------------------------------------------------- +# Build one round of the chip (free input state + free message). +# --------------------------------------------------------------------------- +def build_g(cir, v, a, b, c, d, mx, my, bug, gflag): + b_first = c if (bug == "swap_g_operand" and gflag) else b # WRONG: v[c] for v[b] + v[a] = cir.add3(v[a], v[b_first], mx) + v[d] = cir.rotr16(cir.xor(v[d], v[a])) + v[c] = cir.add2(v[c], v[d]) + v[b] = cir.rotr(cir.xor(v[b], v[c]), 12, + wrong_amount=(bug == "rot_wrong_amount" and gflag)) + v[a] = cir.add3(v[a], v[b], my, + drop_bool=(bug == "drop_carry_bool" and gflag)) + v[d] = cir.rotr8(cir.xor(v[d], v[a])) + v[c] = cir.add2(v[c], v[d]) + v[b] = cir.rotr(cir.xor(v[b], v[c]), 7) + + +def build_round(cir, v, m, bug=None, bug_first_g_only=True): + for gi, (a, b, c, d, ix, iy) in enumerate(G_CALLS): + gflag = (gi == 0) if bug_first_g_only else True + build_g(cir, v, a, b, c, d, m[ix], m[iy], bug, gflag) + + +def build_compress(cir, h, m, tlo, thi, bl, fl, rounds, bug=None): + iv = list(IV) + if bug == "wrong_iv": + iv[0] ^= 1 # negative control + v = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], + cir.const_word(iv[0]), cir.const_word(iv[1]), + cir.const_word(iv[2]), cir.const_word(iv[3]), + tlo, thi, bl, fl] + perm = list(MSG_PERMUTATION) + if bug == "wrong_msg_index": + perm[0], perm[1] = perm[1], perm[0] # negative control + schedule = list(m) + for r in range(rounds): + # only inject round-logic bugs in round 0's first G + rbug = bug if (r == 0 and bug in + ("rot_wrong_amount", "swap_g_operand", "drop_carry_bool")) else None + build_round(cir, v, schedule, bug=rbug) + if r < rounds - 1: + schedule = [schedule[perm[i]] for i in range(16)] + out = [None] * 16 + for i in range(8): + out[i] = cir.xor(v[i], v[i + 8]) + out[i + 8] = cir.xor(v[i + 8], h[i]) + if bug == "drop_ff_xor" and i == 0: + out[0] = cir.fresh_word() # dropped: output left free + return out + + +# =========================================================================== +# Checks +# =========================================================================== +def check_g(bug=None, timeout_ms=0): + """Single G-function vs reference G. Free 4 state words + 2 message words. + UNSAT = the G quarter-round is correctly & tightly constrained. A round is a + fixed composition of 8 G-calls on specified indices, so a correct G under + arbitrary inputs => correct round (the chaining argument).""" + tag = "g" + (f"_{bug}" if bug else "") + cir = Circuit(tag, bug) + va, vb, vc, vd = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + mx, my = cir.fresh_word(), cir.fresh_word() + v = [None] * 16 + v[0], v[1], v[2], v[3] = va, vb, vc, vd + build_g(cir, v, 0, 1, 2, 3, mx, my, bug, gflag=True) + rv = [cir.word32(va), cir.word32(vb), cir.word32(vc), cir.word32(vd)] + bref_g(rv, 0, 1, 2, 3, cir.word32(mx), cir.word32(my)) + s = Solver() + if timeout_ms: + s.set("timeout", timeout_ms) + s.add(And(*cir.C)) + s.add(Or(cir.word32(v[0]) != rv[0], cir.word32(v[1]) != rv[1], + cir.word32(v[2]) != rv[2], cir.word32(v[3]) != rv[3])) + return s.check() + + +def check_round(bug=None, timeout_ms=0): + """Round circuit vs reference round. Free state + free message. UNSAT = correct.""" + tag = "rnd" + (f"_{bug}" if bug else "") + cir = Circuit(tag, bug) + state = [cir.fresh_word() for _ in range(16)] + msg = [cir.fresh_word() for _ in range(16)] + v = list(state) + build_round(cir, v, msg, bug=bug) + ref = bref_round_only([cir.word32(w) for w in state], + [cir.word32(w) for w in msg]) + s = Solver() + if timeout_ms: + s.set("timeout", timeout_ms) + s.add(And(*cir.C)) + s.add(Or(*[cir.word32(v[i]) != ref[i] for i in range(16)])) + return s.check() + + +def check_compress(rounds, bug=None, timeout_ms=0): + """Full compression vs reference. UNSAT = correct.""" + tag = f"cmp{rounds}" + (f"_{bug}" if bug else "") + cir = Circuit(tag, bug) + h = [cir.fresh_word() for _ in range(8)] + m = [cir.fresh_word() for _ in range(16)] + tlo, thi, bl, fl = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + out = build_compress(cir, h, m, tlo, thi, bl, fl, rounds, bug=bug) + ref = bref_compress([cir.word32(w) for w in h], [cir.word32(w) for w in m], + cir.word32(tlo), cir.word32(thi), cir.word32(bl), + cir.word32(fl), rounds) + s = Solver() + if timeout_ms: + s.set("timeout", timeout_ms) + s.add(And(*cir.C)) + s.add(Or(*[cir.word32(out[i]) != ref[i] for i in range(16)])) + return s.check() + + +def positive_control_compress(rounds, h_i, m_i, tlo_i, thi_i, bl_i, fl_i, out_i): + """Non-vacuity + external anchor: pin inputs to a concrete oracle vector, + assert the chip output == the RECORDED oracle output, expect SAT.""" + tag = f"pos{rounds}" + cir = Circuit(tag) + h = [cir.fresh_word() for _ in range(8)] + m = [cir.fresh_word() for _ in range(16)] + tlo, thi, bl, fl = (cir.fresh_word(), cir.fresh_word(), + cir.fresh_word(), cir.fresh_word()) + out = build_compress(cir, h, m, tlo, thi, bl, fl, rounds) + s = Solver() + s.add(And(*cir.C)) + # pin inputs + for wi, val in zip(h, h_i): + s.add(cir.word32(wi) == BitVecVal(val, 32)) + for wi, val in zip(m, m_i): + s.add(cir.word32(wi) == BitVecVal(val, 32)) + s.add(cir.word32(tlo) == BitVecVal(tlo_i, 32)) + s.add(cir.word32(thi) == BitVecVal(thi_i, 32)) + s.add(cir.word32(bl) == BitVecVal(bl_i, 32)) + s.add(cir.word32(fl) == BitVecVal(fl_i, 32)) + # pin output to the recorded oracle vector + for wi, val in zip(out, out_i): + s.add(cir.word32(wi) == BitVecVal(val, 32)) + return s.check() + + +# =========================================================================== +# WIDTH AUDIT: field-level (mod p) bound-necessity for the shift identity and +# the add carry. A wide-BV model cannot show these (2^16 / 2^32 are zero +# divisors mod 2^n); the prime field is required, exactly as +# ../keccak-verify/hwsl_inline_test.py Part 2 demonstrates. +# =========================================================================== +def field_shift_bound(r, in_hw, drop_sll_bound): + """hw*2^r == SLLC*2^16 + SLL (mod p). SLL bounded to [0,2^16) unless dropped. + Returns 'unsat' if SLL is pinned to the honest value; 'sat' if ambiguous.""" + s = Solver() + if drop_sll_bound: + SLL = Int("SLL"); s.add(SLL >= 0, SLL < P) # UNBOUNDED field elt + else: + lo, hi = Int("sll_lo"), Int("sll_hi") + s.add(lo >= 0, lo < 256, hi >= 0, hi < 256) # AreBytes: 2 bytes + SLL = lo + 256 * hi + SLLC = Int("SLLC") + s.add(SLLC >= 0, SLLC < 2**16) # loose 16-bit is fine + s.add((in_hw * (2 ** r) - SLLC * (2 ** 16) - SLL) % P == 0) + sll_ref = (in_hw * (2 ** r)) % (2 ** 16) + s.add(SLL != sll_ref) # a WRONG SLL admissible? + return str(s.check()) + + +def field_add_carry(a, b, m3, drop_bool): + """3-op: a+b+m == s + 2^32*(c1+c2) (mod p). s in [0,2^32). carries in {0,1} + unless dropped. Returns 'unsat' if s pinned to honest, 'sat' if ambiguous.""" + s = Solver() + S = Int("S"); s.add(S >= 0, S < 2**32) + if drop_bool: + c1 = Int("c1"); s.add(c1 >= 0, c1 < P) # UNBOUNDED + csum = c1 + else: + c1, c2 = Int("c1"), Int("c2") + s.add(Or(c1 == 0, c1 == 1), Or(c2 == 0, c2 == 1)) + csum = c1 + c2 + s.add((a + b + m3 - S - (2**32) * csum) % P == 0) + s_ref = (a + b + m3) % (2**32) + s.add(S != s_ref) + return str(s.check()) + + +# =========================================================================== +def load_canonical_6round(): + here = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(here, "..", "blake3-oracle", "canonical_6round_vectors.json") + with open(path) as f: + return json.load(f) + + +def gen_7round_vector(): + """Concrete 7-round compression vector from the validated oracle itself.""" + here = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, os.path.join(here, "..", "blake3-oracle")) + import blake3_ref as ora + import random + rng = random.Random(12345) + h = [rng.randrange(0, 1 << 32) for _ in range(8)] + m = [rng.randrange(0, 1 << 32) for _ in range(16)] + t = rng.randrange(0, 1 << 64) + bl = rng.randrange(0, 65) + fl = rng.randrange(0, 128) + out = ora.compress(h, m, t, bl, fl, rounds=7) + return h, m, t & MASK32, (t >> 32) & MASK32, bl, fl, out + + +def main(): + full = "--full" in sys.argv + print("=" * 70) + print("BLAKE3 compression-chip z3 gate") + print("=" * 70) + + # --- MAIN CHECK 0: single G (fundamental unit; covers every G/round) -- + print("\n=== MAIN CHECK 0: one G-function, free inputs (covers every G) ===") + g = check_g() + print(f" G (clean) -> {g} (want unsat)") + g_ok = (g == unsat) + + # --- MAIN CHECK 1: init-state layout + feed-forward (rounds=0) -------- + # Tiny & symbolic: v = initial state, then the feed-forward XORs. Isolates + # the h/IV/counter-split placement and out[i]=v[i]^v[i+8], out[i+8]=v[i+8]^h[i]. + print("\n=== MAIN CHECK 1: init-state + feed-forward (rounds=0, symbolic) ===") + r0 = check_compress(0) + print(f" compress rounds=0 -> {r0} (want unsat)") + wrapper_ok = (r0 == unsat) + + # --- Heavy symbolic multi-round UNSATs: BONUS, gated behind --full ---- + round_ok = None + full6 = full7 = full2 = None + if full: + print("\n=== MAIN CHECK 2 (--full): one round, free state+message ===") + rr = check_round(timeout_ms=1_800_000) + print(f" round (clean) -> {rr} (want unsat)") + round_ok = (rr == unsat) + print("\n=== MAIN CHECK 3 (--full): compression rounds=2 (permutation+chaining) ===") + full2 = check_compress(2, timeout_ms=1_800_000) + print(f" compress rounds=2 -> {full2} (want unsat)") + print("\n=== MAIN CHECK 4 (--full): FULL compression rounds=6 and rounds=7 ===") + full6 = check_compress(6, timeout_ms=2_400_000) + print(f" compress rounds=6 -> {full6} (want unsat)") + full7 = check_compress(7, timeout_ms=2_400_000) + print(f" compress rounds=7 -> {full7} (want unsat)") + else: + print("\n=== Heavy symbolic multi-round UNSATs skipped (pass --full) ===") + print(" G-unsat + fixed G-composition (chaining) already prove every round;") + print(" rounds=0 proves init+feed-forward; the message permutation is") + print(" proven load-bearing by the wrong_msg_index control and exercised") + print(" concretely by the full 6-/7-round positive controls below.") + + # --- NEGATIVE CONTROLS (must all be SAT) ----------------------------- + print("\n=== NEGATIVE CONTROLS — STRUCTURAL bugs (BV-observable, must be SAT) ===") + # NB: 'dropped carry booleanity' is deliberately NOT here. Dropping a carry + # column's booleanity is a FIELD-level soundness bug: an unconstrained + # committed column is a full field element, but in a *bounded BV* model the + # 8-bit carry + the s in [0,2^32) byte-range still pins s, so BV reports + # UNSAT. It is demonstrated correctly in the WIDTH AUDIT below (drop -> SAT), + # exactly as ../keccak-verify/hwsl_inline_test.py Part 2 requires the prime + # field to show HWSL bound-necessity. This is a feature: the gate separates + # BV-observable logic bugs from field-only soundness bugs. + controls = {} + controls["rot_wrong_amount"] = check_g(bug="rot_wrong_amount") # wrong rotation amount + controls["swap_g_operand"] = check_g(bug="swap_g_operand") # swapped G operand + controls["wrong_iv"] = check_compress(1, bug="wrong_iv") # wrong IV constant + controls["drop_ff_xor"] = check_compress(1, bug="drop_ff_xor") # dropped feed-forward XOR + controls["wrong_msg_index"] = check_compress(2, bug="wrong_msg_index") # wrong msg-schedule index + for name, res in controls.items(): + print(f" bug={name:18s} -> {res} (want sat)") + controls_ok = all(res == sat for res in controls.values()) + + # --- POSITIVE CONTROLS (external anchor: pin to oracle vectors) ------- + print("\n=== POSITIVE CONTROLS (pin input+output to oracle vectors -> SAT) ===") + vecs = load_canonical_6round() + pos_ok = True + for vec in vecs[:3]: + res = positive_control_compress( + 6, vec["h"], vec["m"], vec["t"] & MASK32, (vec["t"] >> 32) & MASK32, + vec["block_len"], vec["flags"], vec["out"]) + ok = (res == sat) + pos_ok &= ok + print(f" 6round seed={vec['seed']} (canonical) -> {res} (want sat)") + h7, m7, tlo7, thi7, bl7, fl7, out7 = gen_7round_vector() + res7 = positive_control_compress(7, h7, m7, tlo7, thi7, bl7, fl7, out7) + pos_ok &= (res7 == sat) + print(f" 7round (oracle-generated) -> {res7} (want sat)") + + # --- WIDTH AUDIT (field-level bound-necessity) ----------------------- + # These are the FIELD-level negative controls (BV provably cannot show them, + # since 2^16 / 2^32 are zero divisors mod 2^n). 'DROP -> sat' == the bug is + # exploitable in the prime field; 'present -> unsat' == the range check pins + # the value. Includes the 'dropped carry booleanity' control (team-lead #4). + print("\n=== WIDTH AUDIT + FIELD-LEVEL NEGATIVE CONTROLS (mod p bound necessity) ===") + a_sh = field_shift_bound(9, 0x9C3A, drop_sll_bound=False) + b_sh = field_shift_bound(9, 0x9C3A, drop_sll_bound=True) + print(f" shift r=9 AreBytes SLL bound present -> {a_sh} (want unsat: pinned)") + print(f" shift r=9 DROP SLL bound (neg ctrl) -> {b_sh} (want sat: forgeable)") + a_ad = field_add_carry(0xF0000000, 0xF0000000, 0xF0000000, drop_bool=False) + b_ad = field_add_carry(0xF0000000, 0xF0000000, 0xF0000000, drop_bool=True) + print(f" 3-add carry booleanity present -> {a_ad} (want unsat: pinned)") + print(f" 3-add DROP booleanity (neg ctrl #4) -> {b_ad} (want sat: forgeable)") + audit_ok = (a_sh == "unsat" and b_sh == "sat" and a_ad == "unsat" and b_ad == "sat") + + # --- VERDICT ---------------------------------------------------------- + print("\n" + "=" * 70) + print("VERDICT") + print("=" * 70) + print(f" G-function UNSAT (covers all G) : {g_ok}") + print(f" init+feed-forward UNSAT (rounds=0): {wrapper_ok}") + if full: + print(f" round UNSAT (direct) : {round_ok}") + print(f" compress rounds=2 UNSAT : {full2 == unsat}") + print(f" full 6-round UNSAT : {full6 == unsat}") + print(f" full 7-round UNSAT : {full7 == unsat}") + print(f" negative controls all SAT : {controls_ok}") + print(f" positive controls all SAT : {pos_ok} (full 6-/7-round pipeline, concrete)") + print(f" width audit (bound necessity) : {audit_ok}") + # G correctness + fixed G-composition => round correctness (chaining); + # rounds=0 => init+feed-forward; positive controls run the full pipeline + # concretely; the direct multi-round UNSATs (--full) are bonus confirmation. + base_ok = g_ok and wrapper_ok and controls_ok and pos_ok and audit_ok + full_ok = (not full) or (round_ok and full2 == unsat + and full6 == unsat and full7 == unsat) + ok = base_ok and full_ok + print(f"\n OVERALL: {'PASS' if ok else 'FAIL — investigate above'}") + sys.exit(0 if ok else 1) + + +if __name__ == "__main__": + main() diff --git a/thoughts/blake3/blake3-oracle/ORACLE.md b/thoughts/blake3/blake3-oracle/ORACLE.md new file mode 100644 index 000000000..aee71d1f0 --- /dev/null +++ b/thoughts/blake3/blake3-oracle/ORACLE.md @@ -0,0 +1,368 @@ +# BLAKE3 Compression-Function Oracle + +**Purpose.** Trust anchor for a future BLAKE3 accelerator (precompile chip) in +the Lambda VM STARK prover. Phase 1 = this oracle (the reference `f` + external +validation + chip-contract reuse map). Phase 2 = chip constraint design, gated +against this oracle. The oracle is the reference the chip's trace generation and +constraints will be checked against; a wrong oracle silently poisons everything +downstream, so the validation section is the load-bearing part. + +**Scope.** The reference is the BLAKE3 **compression function** `f`, NOT the full +tree hash. `blake3_ref.py` also contains a full tree hasher, but that exists +*only* so `f` can be validated against the official whole-hash test vectors. The +chip implements `f`; it does not implement the tree. + +--- + +## 1. Validation status: **VALIDATED** + +`test_oracle.py` passes all of the following (re-run: `./venv/bin/python test_oracle.py`): + +| # | External anchor | Independent of our code? | What it covers | Result | +|---|---|---|---|---| +| 1 | Official **`test_vectors.json`** (BLAKE3 team, `test_vectors/test_vectors.json`, fetched from the BLAKE3 GitHub repo) | Yes — authored by the BLAKE3 authors | 35 input lengths (0 … 102400 B) × 3 modes (default hash, keyed hash, derive-key), extended (131-byte) output | **PASS 35/35 × 3** | +| 2 | Official **`blake3` PyPI package** v1.0.9 (the reference Rust implementation via FFI) | Yes — separate codebase | 23 randomised input lengths (0 … 100000 B) × {default, XOF, keyed, derive-key} = 92 differential checks | **PASS 92/92** | +| 3 | **Plonky3 `blake3-air`** compression, ported in `test_oracle.py` from `others/Plonky3/blake3-air/src/generation.rs` | Yes — Plonky3 team, different codebase | 20 000 random `(h, m, t, block_len)` compared at the **compression-function level** (flags = 0, 7 rounds) | **PASS 20000/20000** | + +Anchors 1–2 validate `f` *indirectly but exhaustively*: the whole-hash path +drives `f` under every flag combination (`CHUNK_START`, `CHUNK_END`, `PARENT`, +`ROOT`, `KEYED_HASH`, `DERIVE_KEY_CONTEXT`, `DERIVE_KEY_MATERIAL` and their +compositions) and a wide range of counters (chunk indices 0…99 for the 102400 B +case, plus XOF output-block counters). Anchor 3 validates `f` **directly** at the +compression level against a second independent implementation (flags = 0 only, +since Plonky3's AIR hardcodes `v[15] = 0`). + +The constants were independently cross-checked: `IV` and `MSG_PERMUTATION` in +`blake3_ref.py` match `others/Plonky3/blake3-air/src/constants.rs` (`IV` stored +there as `[lo16, hi16]` pairs; `MSG_PERMUTATION = [2,6,3,10,7,0,4,13,1,11,12,5,9,14,15,8]`). + +> Note: the BLAKE3 repo's `reference_impl/reference_impl.py` returned HTTP 404 at +> fetch time (repo layout changed), so it is **not** used. `f` was written from +> the spec's G-function definition; the three anchors above stand on their own. + +--- + +## 2. Precise definition of both variants + +Everything is on 32-bit unsigned words, little-endian. `⊞` = add mod 2³², +`⊕` = XOR, `x ⋙ n` = rotate-right by `n` bits. + +### 2.1 Constants + +``` +IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] + +MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] +``` + +### 2.2 The G function (quarter round) + +`G(v, a, b, c, d, mx, my)` mutates working-state words `v[a], v[b], v[c], v[d]`: + +``` +v[a] = v[a] ⊞ v[b] ⊞ mx +v[d] = (v[d] ⊕ v[a]) ⋙ 16 +v[c] = v[c] ⊞ v[d] +v[b] = (v[b] ⊕ v[c]) ⋙ 12 +v[a] = v[a] ⊞ v[b] ⊞ my +v[d] = (v[d] ⊕ v[a]) ⋙ 8 +v[c] = v[c] ⊞ v[d] +v[b] = (v[b] ⊕ v[c]) ⋙ 7 +``` + +### 2.3 The round + +Given the (already permuted-for-this-round) 16-word schedule `m`: + +``` +# columns +G(v, 0, 4, 8, 12, m[0], m[1]) +G(v, 1, 5, 9, 13, m[2], m[3]) +G(v, 2, 6, 10, 14, m[4], m[5]) +G(v, 3, 7, 11, 15, m[6], m[7]) +# diagonals +G(v, 0, 5, 10, 15, m[8], m[9]) +G(v, 1, 6, 11, 12, m[10], m[11]) +G(v, 2, 7, 8, 13, m[12], m[13]) +G(v, 3, 4, 9, 14, m[14], m[15]) +``` + +### 2.4 The compression function `f` (parameterised by `ROUNDS`) + +Inputs: `h[0..8]` (chaining value, 8×u32), `m[0..16]` (message block, 16×u32), +`t` (u64 counter), `block_len` (u32, 0..64), `flags` (u32). + +``` +v[0..8] = h[0..8] +v[8..12] = IV[0..4] +v[12] = t mod 2³² # counter low +v[13] = t >> 32 # counter high +v[14] = block_len +v[15] = flags + +schedule = m +for r in 0 .. ROUNDS-1: + round(v, schedule) + schedule = permute(schedule) # trailing permute after last round is unused + +# feed-forward (produces the FULL 16-word output) +for i in 0..8: + out[i] = v[i] ⊕ v[i+8] + out[i+8] = v[i+8] ⊕ h[i] +return out[0..16] +``` + +The truncated 8-word chaining value used inside the tree is `out[0:8]`. The XOF +root output uses **all 16** output words — this is why `f` returns 16 words. + +### 2.5 Variant A — standard: `ROUNDS = 7` + +The function above with `ROUNDS = 7`. This is standard BLAKE3, validated by +anchors 1–3. + +### 2.6 Variant B — nonstandard: `ROUNDS = 6` + +**Exactly** the function in §2.4 with `ROUNDS = 6`: rounds 0..5 are applied, +round `r` mixing `permute^r(m)`, followed by the identical feed-forward. The +ONLY difference from Variant A is the loop bound. This is a **NONSTANDARD** +function; **no external test vectors exist**. Its anchoring is derivative: + +* **(a) Code-diff anchor.** In `blake3_ref.py`, `compress_6round(...)` is literally + `compress(..., rounds=6)` — same IV, same initial-state layout, same G, same + message permutation schedule, same feed-forward. `test_oracle.py` + (`test_6round_derivation`) asserts `compress_6round == compress(rounds=6)` and + that it differs from `ROUNDS=7` on 2000/2000 random inputs. +* **(b) Canonical vectors.** 10 deterministic vectors (fixed seeds 0..9) are + generated and recorded below. These are Variant B's canonical reference going + forward. Full inputs/outputs are in `canonical_6round_vectors.json`. + +#### Canonical 6-round vectors (seeds 0..9) + +Each row: 32-hex-digit words. `out` is the full 16-word output concatenated +(`out[0]` first). Inputs `h` (8 words), `m` (16 words), `t`, `block_len`, +`flags` are in `canonical_6round_vectors.json`; a summary fingerprint is shown +here (`out[0]` and `out[15]`) so the doc alone pins the vectors' identity. + +| seed | t | block_len | flags | out[0] | out[15] | +|---|---|---|---|---|---| +| 0 | 0xb4e1357d4a84eb03 | 42 | 0x34 | 0xced9d1ff | 0xb75f3915 | +| 1 | 0xc74803e31ba16215 | 50 | 0x5e | 0xf2a972e9 | 0xdfb91125 | +| 2 | 0x7604e4b4e73695c3 | 58 | 0x7c | 0x5aa6b114 | 0x775f2f92 | +| 3 | 0x92d3043afcf249f3 | 36 | 0x1f | 0xeed92fab | 0xdc293166 | +| 4 | 0x49c7b59b995253fd | 57 | 0x29 | 0xca00bda3 | 0x7561eb37 | +| 5 | 0x6a3753915c76f18a | 18 | 0x43 | 0x14a9f66f | 0xbb7a485d | +| 6 | 0x390567c27bd6aa42 | 26 | 0x03 | 0x32a6ff70 | 0x2a7a62b2 | +| 7 | 0x12bd4acefaecbd38 | 53 | 0x2a | 0xa632ad45 | 0xf3f33689 | +| 8 | 0x329911da9fbd8735 | 19 | 0x5b | 0x913b2ae1 | 0x3c5a654b | +| 9 | 0xeaeb999b8a2e547e | 64 | 0x15 | 0xf5ee9114 | 0xd18a8b94 | + +(To re-derive: `random.Random(seed)` then draw `h=8×u32, m=16×u32, t=u64, +block_len∈[0,65), flags∈[0,128)` in that order — see +`test_oracle.canonical_6round_vectors`.) + +--- + +## 3. Chip-contract reuse map + +Every primitive op of `f` mapped onto the existing precomputed-table contracts. +Citations are to `prover/src/tables/bitwise.rs` (the 2²⁰-row BITWISE table) and +the KECCAK chips, which are the architectural template for a byte-oriented +delegation chip. + +The BITWISE table (`bitwise.rs:97`, `NUM_ROWS = 256·256·16 = 2²⁰`) is indexed by +`(X: byte, Y: byte, Z: 4-bit)` and provides these receivers +(`bitwise.rs:715` `bus_interactions`): + +* `ByteAlu[opsel, X, Y] → out` — byte AND/OR/XOR (`bitwise.rs:865-921`; `opsel` + ∈ {AND, OR, XOR}). The output column is a table column, so a `ByteAlu` send + **simultaneously range-checks X and Y to be bytes and pins `out` to the exact + result** — no separate range check needed on any of the three. +* `ARE_BYTES[X, Y]` — range-check two bytes (`bitwise.rs:783`; pass `Y=0` for a + single byte). +* `IS_HALF[X + 256·Y]` — range-check a 16-bit halfword (`bitwise.rs:798`). +* `IS_B20[...]` — 20-bit range check (`bitwise.rs:813`). +* `HWSL[X + 256·Y, Z] → [SLL, SLLC]` — halfword shift-left (`bitwise.rs:831`), + where `SLL = (hw << Z) & 0xFFFF`, `SLLC = hw >> (16 - Z)` (`bitwise.rs:135-141`), + `Z ∈ [0,16)`. +* `MSB8`, `MSB16`, `ZERO` — not needed by BLAKE3. + +### 3.1 Op-by-op mapping + +| BLAKE3 primitive | Existing contract | How | Cost | +|---|---|---|---| +| **32-bit XOR** (`v[d]⊕v[a]`, `v[b]⊕v[c]`, feed-forward) | `ByteAlu[XOR]` | 4 byte-XOR lookups per 32-bit word, one per byte, exactly as `keccak_rnd` does θ/χ/ι XORs (`keccak_rnd.rs:692-718`). Inputs & output auto-range-checked by the lookup. | 4 sends / 32-bit XOR | +| **`⋙ 16`** | *free* — byte relabeling | rotr16 permutes bytes `[b0,b1,b2,b3] → [b2,b3,b0,b1]`. **VERIFIED** exhaustively (100k random words). No lookup, no column: just re-address the bytes at the next use. | 0 | +| **`⋙ 8`** | *free* — byte relabeling | rotr8 → `[b1,b2,b3,b0]`. **VERIFIED**. | 0 | +| **`⋙ 12`** | `HWSL` (+ `ARE_BYTES`) | rotr12 = rotl20; per the keccak-ρ pattern, HWSL each of the 2 halfwords by `rnc=4`, then a halfword rotate by `rbc=1`, recombining `newlo = SLL_lo + SLLC_hi`, `newhi = SLL_hi + SLLC_lo` (non-overlapping bit ranges ⇒ add = OR), then swap the two halfwords. **VERIFIED** (50k random). Range-check the 4 output bytes with `ARE_BYTES` (as keccak does on ρ outputs, `keccak_rnd.rs:768-790`). | 2 HWSL + 2 ARE_BYTES / rot | +| **`⋙ 7`** | `HWSL` (+ `ARE_BYTES`) | rotr7 = rotl25; same pattern with `rnc=9`, `rbc=1`. **VERIFIED**. `rnc=9 < 16` fits HWSL's 4-bit `Z`. | 2 HWSL + 2 ARE_BYTES / rot | +| **32-bit add mod 2³²** (2-operand `v[c]⊞v[d]`) | carry-bit polynomial constraint + range-check | Exactly `emit_add_pair`'s low half (`templates.rs:334`): with sum `s` committed and range-checked, `carry = (a + b − s)·2⁻³²` is constrained `carry·(1−carry)=0` (`INV_SHIFT_32 = (2³²)⁻¹`, `templates.rs:26`). Sum bytes are range-checked *for free* because `s` immediately feeds an XOR lookup. | 1 poly constraint / add | +| **3-operand add mod 2³²** (`v[a]⊞v[b]⊞mx`) | carry constraint (see §5 open Q) | `a+b+m < 3·2³²` ⇒ carry ∈ {0,1,2}. Either one virtual `k(k−1)(k−2)=0` (deg 3) or two chained `emit_add_pair` steps (deg ≤ 2). See open question O1. | 1–2 poly constraints / add | +| **message schedule** (`permute` between rounds) | *free* — wiring | Fixed compile-time permutation of the 16 input words per round; round `r` references `permute^r`-indexed message columns. No table, exactly like `keccak_rnd` inlines `KECCAK_RHO` offsets as compile-time constants. **Confirmed.** | 0 | +| **IV constants, flags, block_len, counter split** | constants / direct columns | `IV[0..4] → v[8..12]`, `t` split into `v[12]=t mod 2³²`, `v[13]=t>>32`, `v[14]=block_len`, `v[15]=flags`. Constants inlined; counter split is two committed words range-checked. | ~0 | + +**No BLAKE3 op lacks an existing contract.** All arithmetic reduces to +`ByteAlu[XOR]`, `HWSL`, `ARE_BYTES`, and the `emit_add_pair` carry template — +every one already exercised by the KECCAK chips. The 32-bit-add carry range +checks fit `ARE_BYTES`/`IS_HALF` (the sum's bytes/halfwords), and the carry +itself is a `{0,1}` (or `{0,1,2}`) polynomial bit, not a table lookup. + +### 3.2 Why the two "free" rotations are actually free + +`ByteAlu` and `HWSL` operate at byte / halfword granularity, and the working +state is stored as bytes. A rotate-right by a multiple of 8 is a permutation of +byte positions, so the constraint at the *consuming* site simply reads the bytes +in rotated order (the same trick keccak uses implicitly). Only `⋙12` and `⋙7` +cross byte boundaries and therefore need HWSL. This means **half** of BLAKE3's +rotations cost nothing. + +--- + +## 4. I/O column boundary sketch + +Analogous to keccak's 200-byte state handoff (`keccak.rs`), the chip's +bus-facing tuple. Recommended **granularity: bytes** — because XOR (the dominant +op) needs byte operands and the two byte-aligned rotations are free at byte +granularity; adds read bytes as a linear combination (`AddOperand::from_dword_bl`, +`templates.rs:191`) so byte storage costs them nothing. + +**Chip input** (read from guest memory via the ECALL/MEMW interface, exactly the +keccak pattern `keccak.rs:160-449`: ECALL receiver binds the syscall + timestamp, +a MEMW read of `x10` binds the state pointer, then per-word MEMW reads): + +| field | size | granularity | +|---|---|---| +| `h[0..8]` chaining value | 8 words = 32 B | bytes | +| `m[0..16]` message block | 16 words = 64 B | bytes | +| `t` counter | u64 = 8 B | 2 words (lo, hi), byte-stored | +| `block_len` | u32 | 1 word | +| `flags` | u32 | 1 word | + +**Chip output** (written back to memory): + +| field | size | granularity | +|---|---|---| +| `out[0..16]` | 16 words = 64 B | bytes | + +For the truncated (CV-only) call sites the guest reads back `out[0:8]`; the chip +always produces the full 16 words (the XOF root needs them). + +**Internal handoff (if one-row-per-round).** If the chip mirrors keccak's +round-chip split, a `Blake3Round` bus carries `(timestamp, round_index, +state[16 words as 64 bytes], message[16 words])` from row `r` to row `r+1`, +mirroring `keccak_rnd`'s `(timestamp, round, start[200])` handoff +(`keccak_rnd.rs:441-515`). Note BLAKE3 must also carry the (round-permuted) +message down the rounds, unlike keccak whose round chip has no message input. + +--- + +## 5. Cost estimate & recommended granularity + +Cost model (given): a **committed** cell is expensive; each **bus send** ≈ 1.5 +base cells of aux; **max constraint degree 3** is a hard cap. + +### Per-round work (8 G calls; each G = 2 three-operand adds, 2 two-operand adds, +4 XORs, 4 rotations of which 2 are free): + +| resource | per round | note | +|---|---|---| +| `ByteAlu[XOR]` sends | 8·4·4 = **128** | 4 XORs/G × 4 bytes | +| `HWSL` sends | 8·2·2 = **32** | 2 non-free rots/G × 2 halfwords | +| `ARE_BYTES` (rot-output range checks) | ~**32** | 2 rots/G × 4 bytes ÷ 2-per-send | +| add carry constraints | ~**48** | (16 three-op + 16 two-op adds)/round | +| committed byte-cells (state + G intermediates + carries) | ~**450** | ~10 words/G committed × 8 G × 4 B + input state | + +Bus sends/round ≈ 128 + 32 + 32 ≈ **~190**; aux ≈ 190 × 1.5 ≈ **~290** base +cells; committed ≈ **~450**. Total ≈ **~740 cell-equivalents/round**. + +### Per compression (7 rounds + feed-forward + I/O): + +* XOR lookups: 7·128 + 64 (feed-forward) ≈ **~960** +* HWSL lookups: 7·32 ≈ **~224** +* Range-check sends: ~7·32 + I/O ≈ **~250** +* **Total bus sends ≈ ~1450**, aux ≈ ~2200 base cells +* Committed ≈ 7·450 + I/O ≈ **~3300** base cells +* **Grand total ≈ ~5000–6000 cell-equivalents per compression**, dominated by + the ~960 byte-XOR lookups. + +For scale: a keccak-f permutation is ~24 rounds × 1480 cols. A BLAKE3 +compression is roughly **¼–⅓ of one keccak permutation**. + +### Recommended layout + +BLAKE3 has only **7 rounds** (vs keccak's 24). Two viable shapes: + +* **A. One row per round** (keccak-style): ~450–750 columns/row × 7 rows, plus a + `Blake3Round` internal handoff bus carrying state **and** the permuted message. + Fewer columns, but the message-carrying handoff is extra bus traffic keccak + doesn't have. +* **B. One row per compression** (fully unrolled): ~3000–3500 columns in a single + row; no internal handoff bus, no round-index bookkeeping. The message schedule + is pure compile-time wiring so unrolling is natural. + +**Recommendation: start with B (one row per compression).** With only 7 rounds +the column count (~3k) is comparable to keccak's per-round width, and eliminating +the internal state+message handoff bus removes the biggest source of aux cost and +constraint complexity. Revisit A only if the committed width dominates trace-area +budget. Either way the cell total is the same order (~5–6k). + +--- + +## 6. Open questions for the chip phase + +* **O1 — 3-operand add carry granularity (the main one).** `v[a] = v[a] ⊞ v[b] ⊞ + mx` sums three 32-bit values, so the carry-out is in **{0,1,2}**, not {0,1}. + `emit_add_pair` (`templates.rs:334`) only handles a `{0,1}` carry. Options: + 1. **One virtual carry ∈ {0,1,2}:** commit the sum `s` (range-checked), + `k = (a+b+m−s)·2⁻³²`, constrain `k(k−1)(k−2)=0`. This is **degree 3** — at + the cap. It cannot also be `μ`-gated (that would be degree 4). Feasible only + if padding rows satisfy it ungated (all-zero padding ⇒ `k=0` ⇒ satisfied, + the keccak padding convention — verify this holds for BLAKE3 padding). + 2. **Two chained adds:** `t = a ⊞ b` (carry ∈ {0,1}), then `a' = t ⊞ mx` (carry + ∈ {0,1}), each via `emit_add_pair`, at the cost of one extra committed 32-bit + intermediate `t` per 3-operand add (16 extra words/round). Stays degree ≤ 2, + so it can be `μ`-gated to degree 3. Simpler and gate-friendly. + * **Recommendation:** option 2 (chained adds) unless the extra committed width + is measured to hurt — it keeps every add uniformly `{0,1}`-carry and leaves + degree headroom for `μ`-gating. Decide with a bench once the chip exists. + +* **O2 — carry-bit gating & padding.** Decide whether add-carry and rot + constraints are `μ`-gated (like `keccak_rnd`'s IS_BIT, `keccak_rnd.rs:914`) or + rely on all-zero padding rows satisfying them ungated. This interacts with O1's + degree budget. + +* **O3 — one-row-per-round vs unrolled (§5).** Ties to O2 and to whether the + message schedule is carried on a handoff bus or wired per-row at compile time. + +* **O4 — flags coverage of the direct anchor.** Anchor 3 (Plonky3) only checks + `flags = 0` at the compression level; non-zero flags are validated only through + the whole-hash anchors 1–2. If the chip is ever exercised on raw compression + inputs with arbitrary flags outside a valid tree, add a direct differential + check against the PyPI package's low-level API if/when it exposes `compress` + (it currently does not). + +* **O5 — counter (`t`) width.** The whole-hash anchors drive `t` only up to ~99 + (chunk index) plus small XOF counters. The chip must accept a full u64 `t` + (`v[12]/v[13]` split). Constants and the split are validated structurally, but + if the chip supports enormous counters, add a targeted vector. (Plonky3 anchor + already exercises random full-width u64 `t` — so this is **covered**.) + +* **O6 — endianness at the memory boundary.** BLAKE3 words are little-endian; + the byte-granular I/O sketch (§4) assumes LE byte order in memory. Confirm + against the guest's `blake3` calling convention when wiring MEMW. + +--- + +## 7. File manifest + +``` +blake3-oracle/ +├── blake3_ref.py # reference f (ROUNDS-parameterised) + 6-round variant + tree hasher +├── test_oracle.py # anchors 1-3 + 6-round derivation + canonical-vector emitter +├── ORACLE.md # this document +├── official_test_vectors.json # BLAKE3 team vectors (fetched, unmodified) +├── canonical_6round_vectors.json # 10 canonical Variant-B vectors (generated) +└── venv/ # python venv with the official `blake3` pkg (anchor 2) +``` + +No repository files were modified. diff --git a/thoughts/blake3/blake3-oracle/blake3_ref.py b/thoughts/blake3/blake3-oracle/blake3_ref.py new file mode 100644 index 000000000..ee2d48e85 --- /dev/null +++ b/thoughts/blake3/blake3-oracle/blake3_ref.py @@ -0,0 +1,399 @@ +""" +BLAKE3 compression-function ORACLE (reference implementation). + +This is the TRUST ANCHOR for a future BLAKE3 accelerator chip in the Lambda VM +STARK prover. It is written directly from the BLAKE3 specification / reference +design, NOT copied from any implementation, and then validated externally in +`test_oracle.py` against: + - the official BLAKE3 team's `test_vectors.json`, + - the official `blake3` PyPI package (the reference Rust implementation), + - Plonky3's independent `blake3-air` compression implementation. + +Spec sources used while writing this file (all public): + - BLAKE3 paper / spec, section 2.1-2.2 (compression function, G, round). + - The reference message-permutation schedule and IV constants, which also + appear verbatim in the vendored Plonky3 `blake3-air/src/constants.rs` + (IV, MSG_PERMUTATION) — used here only as a cross-check of the constants, + the mixing logic is written from the spec's G-function definition. + +Everything operates on 32-bit unsigned words, little-endian, exactly as BLAKE3 +specifies. +""" + +# --------------------------------------------------------------------------- +# Constants (BLAKE3 spec, section 2.1) +# --------------------------------------------------------------------------- + +# Initialisation vector: the first 8 words of the SHA-256 IV (fractional parts +# of the square roots of the first 8 primes). Identical to SHA-256 / BLAKE2s. +IV = [ + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, +] + +# Message word permutation applied between successive rounds. After each round +# the 16 message words are permuted by this index map; round r therefore mixes +# the original message under permutation^r. (BLAKE3 spec / reference schedule.) +MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] + +# Domain-separation flags (BLAKE3 spec, table of flags). +CHUNK_START = 1 << 0 # 0x01 +CHUNK_END = 1 << 1 # 0x02 +PARENT = 1 << 2 # 0x04 +ROOT = 1 << 3 # 0x08 +KEYED_HASH = 1 << 4 # 0x10 +DERIVE_KEY_CONTEXT = 1 << 5 # 0x20 +DERIVE_KEY_MATERIAL = 1 << 6 # 0x40 + +# Structural sizes. +BLOCK_LEN = 64 # bytes per compression input block (16 words * 4 bytes) +CHUNK_LEN = 1024 # bytes per chunk (16 blocks) +KEY_LEN = 32 # bytes in a key / chaining value (8 words * 4 bytes) +OUT_LEN = 32 # default output length in bytes + +MASK32 = 0xFFFFFFFF + +# Standard round count for BLAKE3. Variant B is the same function with ROUNDS=6. +DEFAULT_ROUNDS = 7 + + +# --------------------------------------------------------------------------- +# 32-bit word primitives (BLAKE3 spec, section 2.1 "G function") +# --------------------------------------------------------------------------- + +def add32(a, b): + """Addition modulo 2^32 (wrapping).""" + return (a + b) & MASK32 + + +def rotr(x, n): + """Rotate the 32-bit word `x` RIGHT by `n` bits. + + BLAKE3's G uses rotation amounts 16, 12, 8, 7. Rotations by 16 and 8 are + byte-aligned (multiples of 8); 12 and 7 are not. The chip-contract reuse + map in ORACLE.md analyses each of these against the HWSL lookup table. + """ + x &= MASK32 + return ((x >> n) | (x << (32 - n))) & MASK32 + + +def g(state, a, b, c, d, mx, my): + """The BLAKE3 quarter-round mixing function G (spec section 2.1). + + Mixes two message words `mx`, `my` into four state words at indices + a, b, c, d of the 16-word working state. Two "half rounds" of the form + add / xor+rotate: + + v[a] = v[a] + v[b] + mx + v[d] = (v[d] ^ v[a]) >>> 16 + v[c] = v[c] + v[d] + v[b] = (v[b] ^ v[c]) >>> 12 + v[a] = v[a] + v[b] + my + v[d] = (v[d] ^ v[a]) >>> 8 + v[c] = v[c] + v[d] + v[b] = (v[b] ^ v[c]) >>> 7 + """ + state[a] = add32(add32(state[a], state[b]), mx) + state[d] = rotr(state[d] ^ state[a], 16) + state[c] = add32(state[c], state[d]) + state[b] = rotr(state[b] ^ state[c], 12) + state[a] = add32(add32(state[a], state[b]), my) + state[d] = rotr(state[d] ^ state[a], 8) + state[c] = add32(state[c], state[d]) + state[b] = rotr(state[b] ^ state[c], 7) + + +def round_fn(state, m): + """One BLAKE3 round: 4 column mixes then 4 diagonal mixes (spec 2.1). + + `m` is the (already-permuted for this round) 16-word message schedule. + The G calls consume message words m[0..16] in order. + """ + # Mix the columns. + g(state, 0, 4, 8, 12, m[0], m[1]) + g(state, 1, 5, 9, 13, m[2], m[3]) + g(state, 2, 6, 10, 14, m[4], m[5]) + g(state, 3, 7, 11, 15, m[6], m[7]) + # Mix the diagonals. + g(state, 0, 5, 10, 15, m[8], m[9]) + g(state, 1, 6, 11, 12, m[10], m[11]) + g(state, 2, 7, 8, 13, m[12], m[13]) + g(state, 3, 4, 9, 14, m[14], m[15]) + + +def permute(m): + """Apply MSG_PERMUTATION to a 16-word message list, returning a new list.""" + return [m[MSG_PERMUTATION[i]] for i in range(16)] + + +# --------------------------------------------------------------------------- +# The compression function `f` (BLAKE3 spec, section 2.2) +# --------------------------------------------------------------------------- + +def compress(chaining_value, block_words, counter, block_len, flags, + rounds=DEFAULT_ROUNDS): + """BLAKE3 compression function. + + Inputs: + chaining_value : list of 8 u32 words (h[0..8]) + block_words : list of 16 u32 words (m[0..16]) + counter : u64 block counter t + block_len : u32 number of input bytes in this block (0..64) + flags : u32 domain-separation flags + rounds : number of rounds (7 = standard, 6 = variant B) + + Returns a list of 16 u32 words: the full compression output. The truncated + 8-word chaining value used elsewhere in the tree is `output[0:8]`. + + The 16-word initial working state v is: + v[0..8] = chaining_value[0..8] + v[8..12] = IV[0..4] + v[12] = counter mod 2^32 (low 32 bits of t) + v[13] = counter >> 32 (high 32 bits of t) + v[14] = block_len + v[15] = flags + Then `rounds` rounds are applied, permuting the message schedule between + rounds. Finally the feed-forward XOR produces the 16-word output: + output[i] = v[i] ^ v[i+8] for i in 0..8 + output[i+8] = v[i+8] ^ chaining_value[i] for i in 0..8 + """ + assert len(chaining_value) == 8 + assert len(block_words) == 16 + assert 0 <= counter < (1 << 64) + + counter_low = counter & MASK32 + counter_high = (counter >> 32) & MASK32 + + state = [ + chaining_value[0], chaining_value[1], chaining_value[2], chaining_value[3], + chaining_value[4], chaining_value[5], chaining_value[6], chaining_value[7], + IV[0], IV[1], IV[2], IV[3], + counter_low & MASK32, counter_high & MASK32, block_len & MASK32, flags & MASK32, + ] + + # Local copy of the message schedule; permuted between rounds. + m = list(block_words) + for r in range(rounds): + round_fn(state, m) + # Permute between rounds. The permutation after the final round is + # never consumed, so applying it only for r < rounds-1 is equivalent; + # we permute between rounds to keep the loop structure obvious. + if r < rounds - 1: + m = permute(m) + + # Feed-forward XOR producing the full 16-word output. + output = [0] * 16 + for i in range(8): + output[i] = state[i] ^ state[i + 8] + output[i + 8] = state[i + 8] ^ chaining_value[i] + return output + + +def compress_cv(chaining_value, block_words, counter, block_len, flags, + rounds=DEFAULT_ROUNDS): + """The truncated 8-word chaining value: first 8 words of `compress`.""" + return compress(chaining_value, block_words, counter, block_len, flags, + rounds)[:8] + + +# =========================================================================== +# Variant B: 6-round BLAKE3 compression. +# +# This is EXACTLY `compress(..., rounds=6)`. It is a NONSTANDARD function with +# no external test vectors; ORACLE.md documents its canonical vectors. The only +# difference from the validated 7-round function is the loop bound `rounds`. +# =========================================================================== + +def compress_6round(chaining_value, block_words, counter, block_len, flags): + """6-round variant of the BLAKE3 compression function (variant B). + + Rounds 0..5 are applied with message permutations 0..5 (i.e. round r mixes + permute^r(block_words)), then the identical feed-forward XOR finalisation. + Everything else — IV, initial state layout, G function, feed-forward — is + bit-for-bit identical to the 7-round function. + """ + return compress(chaining_value, block_words, counter, block_len, flags, + rounds=6) + + +# =========================================================================== +# Full BLAKE3 tree hash, built ON TOP of `compress`. +# +# This exists ONLY so the compression function can be validated against the +# official whole-hash test vectors (which exercise `compress` under every flag +# combination and many counter values). The chip does NOT implement the tree; +# it implements `compress`. Written from the spec's tree/chunk structure. +# =========================================================================== + +def words_from_le_bytes(b): + """Convert a bytes object (len multiple of 4) into a list of u32 words.""" + assert len(b) % 4 == 0 + return [int.from_bytes(b[i:i + 4], "little") for i in range(0, len(b), 4)] + + +def le_bytes_from_words(words): + return b"".join((w & MASK32).to_bytes(4, "little") for w in words) + + +class _Output: + """A not-yet-finalised node (chunk or parent). Can emit a chaining value + or an extendable root output (spec section 2.3, XOF).""" + + def __init__(self, input_cv, block_words, counter, block_len, flags, rounds): + self.input_cv = input_cv + self.block_words = block_words + self.counter = counter + self.block_len = block_len + self.flags = flags + self.rounds = rounds + + def chaining_value(self): + return compress(self.input_cv, self.block_words, self.counter, + self.block_len, self.flags, self.rounds)[:8] + + def root_output_bytes(self, out_len): + out = bytearray() + counter = 0 + while len(out) < out_len: + words = compress(self.input_cv, self.block_words, counter, + self.block_len, self.flags | ROOT, self.rounds) + # The ROOT output uses ALL 16 output words (this is why compress + # returns 16 words rather than the truncated 8). + out += le_bytes_from_words(words) + counter += 1 + return bytes(out[:out_len]) + + +class _ChunkState: + def __init__(self, key_words, chunk_counter, flags, rounds): + self.cv = list(key_words) + self.chunk_counter = chunk_counter + self.block = b"" + self.blocks_compressed = 0 + self.flags = flags + self.rounds = rounds + + def _start_flag(self): + return CHUNK_START if self.blocks_compressed == 0 else 0 + + def update(self, data): + while data: + if len(self.block) == BLOCK_LEN: + block_words = words_from_le_bytes(self.block) + self.cv = compress(self.cv, block_words, self.chunk_counter, + BLOCK_LEN, self.flags | self._start_flag(), + self.rounds)[:8] + self.blocks_compressed += 1 + self.block = b"" + take = min(BLOCK_LEN - len(self.block), len(data)) + self.block += data[:take] + data = data[take:] + + def output(self): + block_words = words_from_le_bytes(self.block + b"\x00" * (BLOCK_LEN - len(self.block))) + return _Output(self.cv, block_words, self.chunk_counter, len(self.block), + self.flags | self._start_flag() | CHUNK_END, self.rounds) + + +def _parent_output(left_cv, right_cv, key_words, flags, rounds): + block_words = left_cv + right_cv # 16 words + return _Output(list(key_words), block_words, 0, BLOCK_LEN, flags | PARENT, rounds) + + +class Blake3Hasher: + """Minimal BLAKE3 tree hasher over the reference `compress`. + + Supports the three official modes (default hash, keyed hash, derive-key) + and extendable output, so it can be checked against `test_vectors.json`. + """ + + def __init__(self, key_words, flags, rounds=DEFAULT_ROUNDS): + self.key_words = list(key_words) + self.flags = flags + self.rounds = rounds + self.chunk_state = _ChunkState(self.key_words, 0, flags, rounds) + self.cv_stack = [] # list of 8-word chaining values + + @classmethod + def default(cls, rounds=DEFAULT_ROUNDS): + return cls(IV, 0, rounds) + + @classmethod + def keyed(cls, key32, rounds=DEFAULT_ROUNDS): + assert len(key32) == KEY_LEN + return cls(words_from_le_bytes(key32), KEYED_HASH, rounds) + + @classmethod + def derive_key(cls, context_string, rounds=DEFAULT_ROUNDS): + # Phase 1: hash the context string in DERIVE_KEY_CONTEXT mode to get a + # 32-byte context key; Phase 2: keyed-hash the material with that key + # under DERIVE_KEY_MATERIAL. + ctx_hasher = cls(IV, DERIVE_KEY_CONTEXT, rounds) + ctx_hasher.update(context_string.encode("utf-8") if isinstance(context_string, str) else context_string) + context_key = ctx_hasher.finalize(KEY_LEN) + return cls(words_from_le_bytes(context_key), DERIVE_KEY_MATERIAL, rounds) + + def _add_chunk_cv(self, new_cv, total_chunks): + # Merge the CV stack following the binary-tree structure. A completed + # subtree is merged whenever the total chunk count is even at that level. + while total_chunks & 1 == 0: + left = self.cv_stack.pop() + new_cv = _parent_output(left, new_cv, self.key_words, self.flags, + self.rounds).chaining_value() + total_chunks >>= 1 + self.cv_stack.append(new_cv) + + def update(self, data): + data = bytes(data) + while data: + if len(self.chunk_state.block) == BLOCK_LEN and \ + self.chunk_state.blocks_compressed == CHUNK_LEN // BLOCK_LEN - 1: + # current chunk is full: finalise it and start a new one. + chunk_cv = self.chunk_state.output().chaining_value() + total_chunks = self.chunk_state.chunk_counter + 1 + self._add_chunk_cv(chunk_cv, total_chunks) + self.chunk_state = _ChunkState(self.key_words, total_chunks, + self.flags, self.rounds) + # How many bytes still fit in the current chunk. + want = CHUNK_LEN - self._chunk_len() + take = min(want, len(data)) + self.chunk_state.update(data[:take]) + data = data[take:] + + def _chunk_len(self): + return self.chunk_state.blocks_compressed * BLOCK_LEN + len(self.chunk_state.block) + + def finalize(self, out_len=OUT_LEN): + # Walk the current chunk's output up the CV stack, XORing/parenting all + # the way to the root, and emit the root output. + output = self.chunk_state.output() + parent_nodes_remaining = len(self.cv_stack) + while parent_nodes_remaining > 0: + parent_nodes_remaining -= 1 + left = self.cv_stack[parent_nodes_remaining] + output = _parent_output(left, output.chaining_value(), + self.key_words, self.flags, self.rounds) + return output.root_output_bytes(out_len) + + +def blake3_hash(data, out_len=OUT_LEN, rounds=DEFAULT_ROUNDS): + h = Blake3Hasher.default(rounds) + h.update(data) + return h.finalize(out_len) + + +def blake3_keyed_hash(key32, data, out_len=OUT_LEN, rounds=DEFAULT_ROUNDS): + h = Blake3Hasher.keyed(key32, rounds) + h.update(data) + return h.finalize(out_len) + + +def blake3_derive_key(context_string, key_material, out_len=OUT_LEN, rounds=DEFAULT_ROUNDS): + h = Blake3Hasher.derive_key(context_string, rounds) + h.update(key_material) + return h.finalize(out_len) + + +if __name__ == "__main__": + # Tiny smoke test: empty-input default hash (compare to test_oracle.py). + print("blake3('') =", blake3_hash(b"").hex()) diff --git a/thoughts/blake3/blake3-oracle/canonical_6round_vectors.json b/thoughts/blake3/blake3-oracle/canonical_6round_vectors.json new file mode 100644 index 000000000..10ee8cbc1 --- /dev/null +++ b/thoughts/blake3/blake3-oracle/canonical_6round_vectors.json @@ -0,0 +1,522 @@ +[ + { + "seed": 0, + "h": [ + 3626764237, + 1806341205, + 2195908194, + 2046968324, + 3900315155, + 2167613558, + 1210484339, + 3246154361 + ], + "m": [ + 3874773259, + 1332073689, + 3134603515, + 2937688618, + 432508404, + 1864753826, + 3921352636, + 2048741382, + 1118805955, + 60308648, + 3726325546, + 3738645480, + 2437440079, + 4155553746, + 1924014660, + 4006490763 + ], + "t": 13033757608824335107, + "block_len": 42, + "flags": 52, + "out": [ + 3470381567, + 3259559595, + 3171982207, + 2434484470, + 2453496512, + 3624177727, + 1500783166, + 2857307264, + 2908815487, + 3037433307, + 3879152609, + 869521091, + 1118447691, + 3315752744, + 2041348976, + 3076471061 + ] + }, + { + "seed": 1, + "h": [ + 3280387012, + 1095513148, + 1930549411, + 2798570523, + 3387541014, + 403123852, + 3589583794, + 1912923437 + ], + "m": [ + 4059906722, + 3871601465, + 131383004, + 2325348894, + 1001090105, + 92297589, + 2758633299, + 3693442237, + 2878940490, + 1302957853, + 3790218436, + 2170177477, + 148287319, + 3424825176, + 743061144, + 1609337231 + ], + "t": 14359731685826847253, + "block_len": 50, + "flags": 94, + "out": [ + 4071191273, + 2180888812, + 1086656188, + 1268894457, + 2666129712, + 1796871858, + 3910496071, + 2829038646, + 2734036659, + 310856722, + 813072437, + 3759806425, + 3202728316, + 3592162272, + 809631558, + 3753447717 + ] + }, + { + "seed": 2, + "h": [ + 242886303, + 364522461, + 3588440356, + 1323436024, + 2602510382, + 2606193617, + 4077622522, + 117874757 + ], + "m": [ + 1632151663, + 2258090960, + 2407373688, + 1014142328, + 102469680, + 1396478261, + 2191394736, + 3837860530, + 3422057796, + 3276568223, + 1519503515, + 4131333072, + 3238422834, + 2277860467, + 2104593779, + 3972123491 + ], + "t": 8504173462006699459, + "block_len": 58, + "flags": 124, + "out": [ + 1520873748, + 3386274828, + 2268646132, + 2891926386, + 2680601054, + 1060043663, + 2360846610, + 4103578245, + 1023812198, + 2132949004, + 2949933306, + 304921216, + 1147868525, + 2990135490, + 3286938319, + 2002726802 + ] + }, + { + "seed": 3, + "h": [ + 2337446730, + 2593816829, + 3596902313, + 1006443827, + 2045921456, + 646892613, + 2726705791, + 2247046192 + ], + "m": [ + 3183652505, + 275012945, + 2538753386, + 3717411168, + 3774472248, + 3956088670, + 4018314376, + 3774703581, + 418563100, + 583981819, + 931951836, + 1292897679, + 2512874164, + 2509342356, + 3883517040, + 3989790985 + ], + "t": 10579804601021778419, + "block_len": 36, + "flags": 31, + "out": [ + 4007210923, + 328045400, + 2438725180, + 326208257, + 3037127287, + 3191867341, + 897875462, + 3457968278, + 1392116149, + 1252158200, + 2970061409, + 743537389, + 2693293984, + 3933130730, + 1832113072, + 3693687142 + ] + }, + { + "seed": 4, + "h": [ + 1013818839, + 1701057193, + 665600858, + 285680177, + 3942586889, + 3286348376, + 2305023086, + 456053774 + ], + "m": [ + 3983477513, + 3464545456, + 3437897285, + 830799655, + 1330795424, + 3779789200, + 2602114036, + 2884935804, + 2173054921, + 763602979, + 2034044485, + 1289545638, + 3903568191, + 3789523705, + 2183442722, + 1777884721 + ], + "t": 5316417565031027709, + "block_len": 57, + "flags": 41, + "out": [ + 3389046179, + 2216925754, + 3888680557, + 866690006, + 165466574, + 2712732178, + 4102951254, + 2399377685, + 2315607722, + 4284158421, + 3072657499, + 773501543, + 1793536573, + 3003084712, + 1896007841, + 1969351479 + ] + }, + { + "seed": 5, + "h": [ + 2675342405, + 3185950873, + 4051686260, + 2787324501, + 3869338171, + 486215926, + 1059022248, + 2335435112 + ], + "m": [ + 2465058629, + 930847394, + 1200367645, + 3288765765, + 3423720279, + 2651938379, + 544169062, + 3742654890, + 4219466551, + 3746962816, + 1242556253, + 4129516530, + 879521323, + 2966284567, + 3838591282, + 1283288560 + ], + "t": 7653677975526109578, + "block_len": 18, + "flags": 67, + "out": [ + 346682991, + 270262248, + 2601144541, + 3997938779, + 2056340738, + 2008238187, + 1505739028, + 2712480509, + 3247758822, + 2303640909, + 2906048517, + 2417554421, + 375059928, + 1048950168, + 2028430931, + 3145353309 + ] + }, + { + "seed": 6, + "h": [ + 3530265750, + 1123655737, + 1940104, + 1602711601, + 3307725433, + 1171229348, + 3444200791, + 2929389929 + ], + "m": [ + 2945015643, + 3626164985, + 400010022, + 3437188107, + 3456510285, + 1250623880, + 4086115940, + 1547818437, + 3906320867, + 1552099921, + 2584484726, + 1307063374, + 2530408928, + 2255988210, + 2846451649, + 842776239 + ], + "t": 4108804320044427842, + "block_len": 26, + "flags": 3, + "out": [ + 849805168, + 3271909564, + 3519510472, + 4052162593, + 1913105236, + 2673574855, + 3059096669, + 2568909711, + 3012256441, + 251056470, + 2571889841, + 162028814, + 841094977, + 2913193055, + 1533365974, + 712663730 + ] + }, + { + "seed": 7, + "h": [ + 647892279, + 2795742288, + 2301595691, + 2179419893, + 161042648, + 1862494042, + 300026767, + 1823296038 + ], + "m": [ + 4070378921, + 1703729684, + 4192983756, + 3687093963, + 1243862422, + 776213899, + 2744112455, + 1599435267, + 884585951, + 1349251823, + 1946412080, + 1287489453, + 3411833895, + 1048386555, + 2467131055, + 2255701793 + ], + "t": 1350317716114554168, + "block_len": 53, + "flags": 42, + "out": [ + 2788339013, + 315507188, + 3524996285, + 1987664994, + 1810642625, + 3673881822, + 1405781943, + 2464695899, + 2067943261, + 3789991295, + 1966842759, + 3435464740, + 1773068141, + 3149656659, + 2026915971, + 4092802697 + ] + }, + { + "seed": 8, + "h": [ + 973694259, + 4133025703, + 542587089, + 3027165658, + 365867937, + 899355976, + 2756803948, + 1971964490 + ], + "m": [ + 1946188980, + 3567061697, + 384681428, + 1750902959, + 1109633622, + 270963824, + 1620083717, + 2838299811, + 1453582679, + 2969113350, + 3871375977, + 4063259978, + 832596604, + 2486621942, + 3783693026, + 3771309886 + ], + "t": 3645965004013668149, + "block_len": 19, + "flags": 91, + "out": [ + 2436573921, + 3354865794, + 1172422691, + 1864318850, + 548333301, + 3673300372, + 4072793263, + 3573011628, + 1151623047, + 4106489061, + 1631493012, + 147739614, + 1341160100, + 1164702434, + 543615615, + 1012557131 + ] + }, + { + "seed": 9, + "h": [ + 1603362544, + 595022250, + 27638352, + 2159432582, + 347096279, + 1627876803, + 3114132053, + 674984870 + ], + "m": [ + 1022254636, + 476516009, + 2535870938, + 1250600339, + 2895821580, + 901471249, + 1207677876, + 3476821989, + 3807057864, + 3776879099, + 2111885832, + 100859404, + 2563432515, + 2485498850, + 872106831, + 358645241 + ], + "t": 16927792517719413886, + "block_len": 64, + "flags": 21, + "out": [ + 4126052628, + 2238491576, + 700329201, + 1614539036, + 2494029070, + 687619623, + 3058576584, + 757884927, + 1778041274, + 211062928, + 3599623221, + 3465651495, + 3893106709, + 1833234406, + 2278011253, + 3515517844 + ] + } +] \ No newline at end of file diff --git a/thoughts/blake3/blake3-oracle/official_test_vectors.json b/thoughts/blake3/blake3-oracle/official_test_vectors.json new file mode 100644 index 000000000..77cd38adb --- /dev/null +++ b/thoughts/blake3/blake3-oracle/official_test_vectors.json @@ -0,0 +1,334 @@ +{ + "key": "whats the Elvish word for friend", + "context_string": "BLAKE3 2019-12-27 16:29:52 test vectors context", + "cases": [ + { + "input_len": 0, + "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d", + "keyed_hash": "92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26b18171a2f22a4b94822c701f107153dba24918c4bae4d2945c20ece13387627d3b73cbf97b797d5e59948c7ef788f54372df45e45e4293c7dc18c1d41144a9758be58960856be1eabbe22c2653190de560ca3b2ac4aa692a9210694254c371e851bc8f", + "derive_key": "2cc39783c223154fea8dfb7c1b1660f2ac2dcbd1c1de8277b0b0dd39b7e50d7d905630c8be290dfcf3e6842f13bddd573c098c3f17361f1f206b8cad9d088aa4a3f746752c6b0ce6a83b0da81d59649257cdf8eb3e9f7d4998e41021fac119deefb896224ac99f860011f73609e6e0e4540f93b273e56547dfd3aa1a035ba6689d89a0" + }, + { + "input_len": 1, + "hash": "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213c3a6cb8bf623e20cdb535f8d1a5ffb86342d9c0b64aca3bce1d31f60adfa137b358ad4d79f97b47c3d5e79f179df87a3b9776ef8325f8329886ba42f07fb138bb502f4081cbcec3195c5871e6c23e2cc97d3c69a613eba131e5f1351f3f1da786545e5", + "keyed_hash": "6d7878dfff2f485635d39013278ae14f1454b8c0a3a2d34bc1ab38228a80c95b6568c0490609413006fbd428eb3fd14e7756d90f73a4725fad147f7bf70fd61c4e0cf7074885e92b0e3f125978b4154986d4fb202a3f331a3fb6cf349a3a70e49990f98fe4289761c8602c4e6ab1138d31d3b62218078b2f3ba9a88e1d08d0dd4cea11", + "derive_key": "b3e2e340a117a499c6cf2398a19ee0d29cca2bb7404c73063382693bf66cb06c5827b91bf889b6b97c5477f535361caefca0b5d8c4746441c57617111933158950670f9aa8a05d791daae10ac683cbef8faf897c84e6114a59d2173c3f417023a35d6983f2c7dfa57e7fc559ad751dbfb9ffab39c2ef8c4aafebc9ae973a64f0c76551" + }, + { + "input_len": 2, + "hash": "7b7015bb92cf0b318037702a6cdd81dee41224f734684c2c122cd6359cb1ee63d8386b22e2ddc05836b7c1bb693d92af006deb5ffbc4c70fb44d0195d0c6f252faac61659ef86523aa16517f87cb5f1340e723756ab65efb2f91964e14391de2a432263a6faf1d146937b35a33621c12d00be8223a7f1919cec0acd12097ff3ab00ab1", + "keyed_hash": "5392ddae0e0a69d5f40160462cbd9bd889375082ff224ac9c758802b7a6fd20a9ffbf7efd13e989a6c246f96d3a96b9d279f2c4e63fb0bdff633957acf50ee1a5f658be144bab0f6f16500dee4aa5967fc2c586d85a04caddec90fffb7633f46a60786024353b9e5cebe277fcd9514217fee2267dcda8f7b31697b7c54fab6a939bf8f", + "derive_key": "1f166565a7df0098ee65922d7fea425fb18b9943f19d6161e2d17939356168e6daa59cae19892b2d54f6fc9f475d26031fd1c22ae0a3e8ef7bdb23f452a15e0027629d2e867b1bb1e6ab21c71297377750826c404dfccc2406bd57a83775f89e0b075e59a7732326715ef912078e213944f490ad68037557518b79c0086de6d6f6cdd2" + }, + { + "input_len": 3, + "hash": "e1be4d7a8ab5560aa4199eea339849ba8e293d55ca0a81006726d184519e647f5b49b82f805a538c68915c1ae8035c900fd1d4b13902920fd05e1450822f36de9454b7e9996de4900c8e723512883f93f4345f8a58bfe64ee38d3ad71ab027765d25cdd0e448328a8e7a683b9a6af8b0af94fa09010d9186890b096a08471e4230a134", + "keyed_hash": "39e67b76b5a007d4921969779fe666da67b5213b096084ab674742f0d5ec62b9b9142d0fab08e1b161efdbb28d18afc64d8f72160c958e53a950cdecf91c1a1bbab1a9c0f01def762a77e2e8545d4dec241e98a89b6db2e9a5b070fc110caae2622690bd7b76c02ab60750a3ea75426a6bb8803c370ffe465f07fb57def95df772c39f", + "derive_key": "440aba35cb006b61fc17c0529255de438efc06a8c9ebf3f2ddac3b5a86705797f27e2e914574f4d87ec04c379e12789eccbfbc15892626042707802dbe4e97c3ff59dca80c1e54246b6d055154f7348a39b7d098b2b4824ebe90e104e763b2a447512132cede16243484a55a4e40a85790038bb0dcf762e8c053cabae41bbe22a5bff7" + }, + { + "input_len": 4, + "hash": "f30f5ab28fe047904037f77b6da4fea1e27241c5d132638d8bedce9d40494f328f603ba4564453e06cdcee6cbe728a4519bbe6f0d41e8a14b5b225174a566dbfa61b56afb1e452dc08c804f8c3143c9e2cc4a31bb738bf8c1917b55830c6e65797211701dc0b98daa1faeaa6ee9e56ab606ce03a1a881e8f14e87a4acf4646272cfd12", + "keyed_hash": "7671dde590c95d5ac9616651ff5aa0a27bee5913a348e053b8aa9108917fe070116c0acff3f0d1fa97ab38d813fd46506089118147d83393019b068a55d646251ecf81105f798d76a10ae413f3d925787d6216a7eb444e510fd56916f1d753a5544ecf0072134a146b2615b42f50c179f56b8fae0788008e3e27c67482349e249cb86a", + "derive_key": "f46085c8190d69022369ce1a18880e9b369c135eb93f3c63550d3e7630e91060fbd7d8f4258bec9da4e05044f88b91944f7cab317a2f0c18279629a3867fad0662c9ad4d42c6f27e5b124da17c8c4f3a94a025ba5d1b623686c6099d202a7317a82e3d95dae46a87de0555d727a5df55de44dab799a20dffe239594d6e99ed17950910" + }, + { + "input_len": 5, + "hash": "b40b44dfd97e7a84a996a91af8b85188c66c126940ba7aad2e7ae6b385402aa2ebcfdac6c5d32c31209e1f81a454751280db64942ce395104e1e4eaca62607de1c2ca748251754ea5bbe8c20150e7f47efd57012c63b3c6a6632dc1c7cd15f3e1c999904037d60fac2eb9397f2adbe458d7f264e64f1e73aa927b30988e2aed2f03620", + "keyed_hash": "73ac69eecf286894d8102018a6fc729f4b1f4247d3703f69bdc6a5fe3e0c84616ab199d1f2f3e53bffb17f0a2209fe8b4f7d4c7bae59c2bc7d01f1ff94c67588cc6b38fa6024886f2c078bfe09b5d9e6584cd6c521c3bb52f4de7687b37117a2dbbec0d59e92fa9a8cc3240d4432f91757aabcae03e87431dac003e7d73574bfdd8218", + "derive_key": "1f24eda69dbcb752847ec3ebb5dd42836d86e58500c7c98d906ecd82ed9ae47f6f48a3f67e4e43329c9a89b1ca526b9b35cbf7d25c1e353baffb590fd79be58ddb6c711f1a6b60e98620b851c688670412fcb0435657ba6b638d21f0f2a04f2f6b0bd8834837b10e438d5f4c7c2c71299cf7586ea9144ed09253d51f8f54dd6bff719d" + }, + { + "input_len": 6, + "hash": "06c4e8ffb6872fad96f9aaca5eee1553eb62aed0ad7198cef42e87f6a616c844611a30c4e4f37fe2fe23c0883cde5cf7059d88b657c7ed2087e3d210925ede716435d6d5d82597a1e52b9553919e804f5656278bd739880692c94bff2824d8e0b48cac1d24682699e4883389dc4f2faa2eb3b4db6e39debd5061ff3609916f3e07529a", + "keyed_hash": "82d3199d0013035682cc7f2a399d4c212544376a839aa863a0f4c91220ca7a6dc2ffb3aa05f2631f0fa9ac19b6e97eb7e6669e5ec254799350c8b8d189e8807800842a5383c4d907c932f34490aaf00064de8cdb157357bde37c1504d2960034930887603abc5ccb9f5247f79224baff6120a3c622a46d7b1bcaee02c5025460941256", + "derive_key": "be96b30b37919fe4379dfbe752ae77b4f7e2ab92f7ff27435f76f2f065f6a5f435ae01a1d14bd5a6b3b69d8cbd35f0b01ef2173ff6f9b640ca0bd4748efa398bf9a9c0acd6a66d9332fdc9b47ffe28ba7ab6090c26747b85f4fab22f936b71eb3f64613d8bd9dfabe9bb68da19de78321b481e5297df9e40ec8a3d662f3e1479c65de0" + }, + { + "input_len": 7, + "hash": "3f8770f387faad08faa9d8414e9f449ac68e6ff0417f673f602a646a891419fe66036ef6e6d1a8f54baa9fed1fc11c77cfb9cff65bae915045027046ebe0c01bf5a941f3bb0f73791d3fc0b84370f9f30af0cd5b0fc334dd61f70feb60dad785f070fef1f343ed933b49a5ca0d16a503f599a365a4296739248b28d1a20b0e2cc8975c", + "keyed_hash": "af0a7ec382aedc0cfd626e49e7628bc7a353a4cb108855541a5651bf64fbb28a7c5035ba0f48a9c73dabb2be0533d02e8fd5d0d5639a18b2803ba6bf527e1d145d5fd6406c437b79bcaad6c7bdf1cf4bd56a893c3eb9510335a7a798548c6753f74617bede88bef924ba4b334f8852476d90b26c5dc4c3668a2519266a562c6c8034a6", + "derive_key": "dc3b6485f9d94935329442916b0d059685ba815a1fa2a14107217453a7fc9f0e66266db2ea7c96843f9d8208e600a73f7f45b2f55b9e6d6a7ccf05daae63a3fdd10b25ac0bd2e224ce8291f88c05976d575df998477db86fb2cfbbf91725d62cb57acfeb3c2d973b89b503c2b60dde85a7802b69dc1ac2007d5623cbea8cbfb6b181f5" + }, + { + "input_len": 8, + "hash": "2351207d04fc16ade43ccab08600939c7c1fa70a5c0aaca76063d04c3228eaeb725d6d46ceed8f785ab9f2f9b06acfe398c6699c6129da084cb531177445a682894f9685eaf836999221d17c9a64a3a057000524cd2823986db378b074290a1a9b93a22e135ed2c14c7e20c6d045cd00b903400374126676ea78874d79f2dd7883cf5c", + "keyed_hash": "be2f5495c61cba1bb348a34948c004045e3bd4dae8f0fe82bf44d0da245a060048eb5e68ce6dea1eb0229e144f578b3aa7e9f4f85febd135df8525e6fe40c6f0340d13dd09b255ccd5112a94238f2be3c0b5b7ecde06580426a93e0708555a265305abf86d874e34b4995b788e37a823491f25127a502fe0704baa6bfdf04e76c13276", + "derive_key": "2b166978cef14d9d438046c720519d8b1cad707e199746f1562d0c87fbd32940f0e2545a96693a66654225ebbaac76d093bfa9cd8f525a53acb92a861a98c42e7d1c4ae82e68ab691d510012edd2a728f98cd4794ef757e94d6546961b4f280a51aac339cc95b64a92b83cc3f26d8af8dfb4c091c240acdb4d47728d23e7148720ef04" + }, + { + "input_len": 63, + "hash": "e9bc37a594daad83be9470df7f7b3798297c3d834ce80ba85d6e207627b7db7b1197012b1e7d9af4d7cb7bdd1f3bb49a90a9b5dec3ea2bbc6eaebce77f4e470cbf4687093b5352f04e4a4570fba233164e6acc36900e35d185886a827f7ea9bdc1e5c3ce88b095a200e62c10c043b3e9bc6cb9b6ac4dfa51794b02ace9f98779040755", + "keyed_hash": "bb1eb5d4afa793c1ebdd9fb08def6c36d10096986ae0cfe148cd101170ce37aea05a63d74a840aecd514f654f080e51ac50fd617d22610d91780fe6b07a26b0847abb38291058c97474ef6ddd190d30fc318185c09ca1589d2024f0a6f16d45f11678377483fa5c005b2a107cb9943e5da634e7046855eaa888663de55d6471371d55d", + "derive_key": "b6451e30b953c206e34644c6803724e9d2725e0893039cfc49584f991f451af3b89e8ff572d3da4f4022199b9563b9d70ebb616efff0763e9abec71b550f1371e233319c4c4e74da936ba8e5bbb29a598e007a0bbfa929c99738ca2cc098d59134d11ff300c39f82e2fce9f7f0fa266459503f64ab9913befc65fddc474f6dc1c67669" + }, + { + "input_len": 64, + "hash": "4eed7141ea4a5cd4b788606bd23f46e212af9cacebacdc7d1f4c6dc7f2511b98fc9cc56cb831ffe33ea8e7e1d1df09b26efd2767670066aa82d023b1dfe8ab1b2b7fbb5b97592d46ffe3e05a6a9b592e2949c74160e4674301bc3f97e04903f8c6cf95b863174c33228924cdef7ae47559b10b294acd660666c4538833582b43f82d74", + "keyed_hash": "ba8ced36f327700d213f120b1a207a3b8c04330528586f414d09f2f7d9ccb7e68244c26010afc3f762615bbac552a1ca909e67c83e2fd5478cf46b9e811efccc93f77a21b17a152ebaca1695733fdb086e23cd0eb48c41c034d52523fc21236e5d8c9255306e48d52ba40b4dac24256460d56573d1312319afcf3ed39d72d0bfc69acb", + "derive_key": "a5c4a7053fa86b64746d4bb688d06ad1f02a18fce9afd3e818fefaa7126bf73e9b9493a9befebe0bf0c9509fb3105cfa0e262cde141aa8e3f2c2f77890bb64a4cca96922a21ead111f6338ad5244f2c15c44cb595443ac2ac294231e31be4a4307d0a91e874d36fc9852aeb1265c09b6e0cda7c37ef686fbbcab97e8ff66718be048bb" + }, + { + "input_len": 65, + "hash": "de1e5fa0be70df6d2be8fffd0e99ceaa8eb6e8c93a63f2d8d1c30ecb6b263dee0e16e0a4749d6811dd1d6d1265c29729b1b75a9ac346cf93f0e1d7296dfcfd4313b3a227faaaaf7757cc95b4e87a49be3b8a270a12020233509b1c3632b3485eef309d0abc4a4a696c9decc6e90454b53b000f456a3f10079072baaf7a981653221f2c", + "keyed_hash": "c0a4edefa2d2accb9277c371ac12fcdbb52988a86edc54f0716e1591b4326e72d5e795f46a596b02d3d4bfb43abad1e5d19211152722ec1f20fef2cd413e3c22f2fc5da3d73041275be6ede3517b3b9f0fc67ade5956a672b8b75d96cb43294b9041497de92637ed3f2439225e683910cb3ae923374449ca788fb0f9bea92731bc26ad", + "derive_key": "51fd05c3c1cfbc8ed67d139ad76f5cf8236cd2acd26627a30c104dfd9d3ff8a82b02e8bd36d8498a75ad8c8e9b15eb386970283d6dd42c8ae7911cc592887fdbe26a0a5f0bf821cd92986c60b2502c9be3f98a9c133a7e8045ea867e0828c7252e739321f7c2d65daee4468eb4429efae469a42763f1f94977435d10dccae3e3dce88d" + }, + { + "input_len": 127, + "hash": "d81293fda863f008c09e92fc382a81f5a0b4a1251cba1634016a0f86a6bd640de3137d477156d1fde56b0cf36f8ef18b44b2d79897bece12227539ac9ae0a5119da47644d934d26e74dc316145dcb8bb69ac3f2e05c242dd6ee06484fcb0e956dc44355b452c5e2bbb5e2b66e99f5dd443d0cbcaaafd4beebaed24ae2f8bb672bcef78", + "keyed_hash": "c64200ae7dfaf35577ac5a9521c47863fb71514a3bcad18819218b818de85818ee7a317aaccc1458f78d6f65f3427ec97d9c0adb0d6dacd4471374b621b7b5f35cd54663c64dbe0b9e2d95632f84c611313ea5bd90b71ce97b3cf645776f3adc11e27d135cbadb9875c2bf8d3ae6b02f8a0206aba0c35bfe42574011931c9a255ce6dc", + "derive_key": "c91c090ceee3a3ac81902da31838012625bbcd73fcb92e7d7e56f78deba4f0c3feeb3974306966ccb3e3c69c337ef8a45660ad02526306fd685c88542ad00f759af6dd1adc2e50c2b8aac9f0c5221ff481565cf6455b772515a69463223202e5c371743e35210bbbbabd89651684107fd9fe493c937be16e39cfa7084a36207c99bea3" + }, + { + "input_len": 128, + "hash": "f17e570564b26578c33bb7f44643f539624b05df1a76c81f30acd548c44b45efa69faba091427f9c5c4caa873aa07828651f19c55bad85c47d1368b11c6fd99e47ecba5820a0325984d74fe3e4058494ca12e3f1d3293d0010a9722f7dee64f71246f75e9361f44cc8e214a100650db1313ff76a9f93ec6e84edb7add1cb4a95019b0c", + "keyed_hash": "b04fe15577457267ff3b6f3c947d93be581e7e3a4b018679125eaf86f6a628ecd86bbe0001f10bda47e6077b735016fca8119da11348d93ca302bbd125bde0db2b50edbe728a620bb9d3e6f706286aedea973425c0b9eedf8a38873544cf91badf49ad92a635a93f71ddfcee1eae536c25d1b270956be16588ef1cfef2f1d15f650bd5", + "derive_key": "81720f34452f58a0120a58b6b4608384b5c51d11f39ce97161a0c0e442ca022550e7cd651e312f0b4c6afb3c348ae5dd17d2b29fab3b894d9a0034c7b04fd9190cbd90043ff65d1657bbc05bfdecf2897dd894c7a1b54656d59a50b51190a9da44db426266ad6ce7c173a8c0bbe091b75e734b4dadb59b2861cd2518b4e7591e4b83c9" + }, + { + "input_len": 129, + "hash": "683aaae9f3c5ba37eaaf072aed0f9e30bac0865137bae68b1fde4ca2aebdcb12f96ffa7b36dd78ba321be7e842d364a62a42e3746681c8bace18a4a8a79649285c7127bf8febf125be9de39586d251f0d41da20980b70d35e3dac0eee59e468a894fa7e6a07129aaad09855f6ad4801512a116ba2b7841e6cfc99ad77594a8f2d181a7", + "keyed_hash": "d4a64dae6cdccbac1e5287f54f17c5f985105457c1a2ec1878ebd4b57e20d38f1c9db018541eec241b748f87725665b7b1ace3e0065b29c3bcb232c90e37897fa5aaee7e1e8a2ecfcd9b51463e42238cfdd7fee1aecb3267fa7f2128079176132a412cd8aaf0791276f6b98ff67359bd8652ef3a203976d5ff1cd41885573487bcd683", + "derive_key": "938d2d4435be30eafdbb2b7031f7857c98b04881227391dc40db3c7b21f41fc18d72d0f9c1de5760e1941aebf3100b51d64644cb459eb5d20258e233892805eb98b07570ef2a1787cd48e117c8d6a63a68fd8fc8e59e79dbe63129e88352865721c8d5f0cf183f85e0609860472b0d6087cefdd186d984b21542c1c780684ed6832d8d" + }, + { + "input_len": 1023, + "hash": "10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11a182d27a591b05592b15607500e1e8dd56bc6c7fc063715b7a1d737df5bad3339c56778957d870eb9717b57ea3d9fb68d1b55127bba6a906a4a24bbd5acb2d123a37b28f9e9a81bbaae360d58f85e5fc9d75f7c370a0cc09b6522d9c8d822f2f28f485", + "keyed_hash": "c951ecdf03288d0fcc96ee3413563d8a6d3589547f2c2fb36d9786470f1b9d6e890316d2e6d8b8c25b0a5b2180f94fb1a158ef508c3cde45e2966bd796a696d3e13efd86259d756387d9becf5c8bf1ce2192b87025152907b6d8cc33d17826d8b7b9bc97e38c3c85108ef09f013e01c229c20a83d9e8efac5b37470da28575fd755a10", + "derive_key": "74a16c1c3d44368a86e1ca6df64be6a2f64cce8f09220787450722d85725dea59c413264404661e9e4d955409dfe4ad3aa487871bcd454ed12abfe2c2b1eb7757588cf6cb18d2eccad49e018c0d0fec323bec82bf1644c6325717d13ea712e6840d3e6e730d35553f59eff5377a9c350bcc1556694b924b858f329c44ee64b884ef00d" + }, + { + "input_len": 1024, + "hash": "42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af71cf8107265ecdaf8505b95d8fcec83a98a6a96ea5109d2c179c47a387ffbb404756f6eeae7883b446b70ebb144527c2075ab8ab204c0086bb22b7c93d465efc57f8d917f0b385c6df265e77003b85102967486ed57db5c5ca170ba441427ed9afa684e", + "keyed_hash": "75c46f6f3d9eb4f55ecaaee480db732e6c2105546f1e675003687c31719c7ba4a78bc838c72852d4f49c864acb7adafe2478e824afe51c8919d06168414c265f298a8094b1ad813a9b8614acabac321f24ce61c5a5346eb519520d38ecc43e89b5000236df0597243e4d2493fd626730e2ba17ac4d8824d09d1a4a8f57b8227778e2de", + "derive_key": "7356cd7720d5b66b6d0697eb3177d9f8d73a4a5c5e968896eb6a6896843027066c23b601d3ddfb391e90d5c8eccdef4ae2a264bce9e612ba15e2bc9d654af1481b2e75dbabe615974f1070bba84d56853265a34330b4766f8e75edd1f4a1650476c10802f22b64bd3919d246ba20a17558bc51c199efdec67e80a227251808d8ce5bad" + }, + { + "input_len": 1025, + "hash": "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444f4c4a22b4b399155358a994e52bf255de60035742ec71bd08ac275a1b51cc6bfe332b0ef84b409108cda080e6269ed4b3e2c3f7d722aa4cdc98d16deb554e5627be8f955c98e1d5f9565a9194cad0c4285f93700062d9595adb992ae68ff12800ab67a", + "keyed_hash": "357dc55de0c7e382c900fd6e320acc04146be01db6a8ce7210b7189bd664ea69362396b77fdc0d2634a552970843722066c3c15902ae5097e00ff53f1e116f1cd5352720113a837ab2452cafbde4d54085d9cf5d21ca613071551b25d52e69d6c81123872b6f19cd3bc1333edf0c52b94de23ba772cf82636cff4542540a7738d5b930", + "derive_key": "effaa245f065fbf82ac186839a249707c3bddf6d3fdda22d1b95a3c970379bcb5d31013a167509e9066273ab6e2123bc835b408b067d88f96addb550d96b6852dad38e320b9d940f86db74d398c770f462118b35d2724efa13da97194491d96dd37c3c09cbef665953f2ee85ec83d88b88d11547a6f911c8217cca46defa2751e7f3ad" + }, + { + "input_len": 2048, + "hash": "e776b6028c7cd22a4d0ba182a8bf62205d2ef576467e838ed6f2529b85fba24a9a60bf80001410ec9eea6698cd537939fad4749edd484cb541aced55cd9bf54764d063f23f6f1e32e12958ba5cfeb1bf618ad094266d4fc3c968c2088f677454c288c67ba0dba337b9d91c7e1ba586dc9a5bc2d5e90c14f53a8863ac75655461cea8f9", + "keyed_hash": "879cf1fa2ea0e79126cb1063617a05b6ad9d0b696d0d757cf053439f60a99dd10173b961cd574288194b23ece278c330fbb8585485e74967f31352a8183aa782b2b22f26cdcadb61eed1a5bc144b8198fbb0c13abbf8e3192c145d0a5c21633b0ef86054f42809df823389ee40811a5910dcbd1018af31c3b43aa55201ed4edaac74fe", + "derive_key": "7b2945cb4fef70885cc5d78a87bf6f6207dd901ff239201351ffac04e1088a23e2c11a1ebffcea4d80447867b61badb1383d842d4e79645d48dd82ccba290769caa7af8eaa1bd78a2a5e6e94fbdab78d9c7b74e894879f6a515257ccf6f95056f4e25390f24f6b35ffbb74b766202569b1d797f2d4bd9d17524c720107f985f4ddc583" + }, + { + "input_len": 2049, + "hash": "5f4d72f40d7a5f82b15ca2b2e44b1de3c2ef86c426c95c1af0b687952256303096de31d71d74103403822a2e0bc1eb193e7aecc9643a76b7bbc0c9f9c52e8783aae98764ca468962b5c2ec92f0c74eb5448d519713e09413719431c802f948dd5d90425a4ecdadece9eb178d80f26efccae630734dff63340285adec2aed3b51073ad3", + "keyed_hash": "9f29700902f7c86e514ddc4df1e3049f258b2472b6dd5267f61bf13983b78dd5f9a88abfefdfa1e00b418971f2b39c64ca621e8eb37fceac57fd0c8fc8e117d43b81447be22d5d8186f8f5919ba6bcc6846bd7d50726c06d245672c2ad4f61702c646499ee1173daa061ffe15bf45a631e2946d616a4c345822f1151284712f76b2b0e", + "derive_key": "2ea477c5515cc3dd606512ee72bb3e0e758cfae7232826f35fb98ca1bcbdf27316d8e9e79081a80b046b60f6a263616f33ca464bd78d79fa18200d06c7fc9bffd808cc4755277a7d5e09da0f29ed150f6537ea9bed946227ff184cc66a72a5f8c1e4bd8b04e81cf40fe6dc4427ad5678311a61f4ffc39d195589bdbc670f63ae70f4b6" + }, + { + "input_len": 3072, + "hash": "b98cb0ff3623be03326b373de6b9095218513e64f1ee2edd2525c7ad1e5cffd29a3f6b0b978d6608335c09dc94ccf682f9951cdfc501bfe47b9c9189a6fc7b404d120258506341a6d802857322fbd20d3e5dae05b95c88793fa83db1cb08e7d8008d1599b6209d78336e24839724c191b2a52a80448306e0daa84a3fdb566661a37e11", + "keyed_hash": "044a0e7b172a312dc02a4c9a818c036ffa2776368d7f528268d2e6b5df19177022f302d0529e4174cc507c463671217975e81dab02b8fdeb0d7ccc7568dd22574c783a76be215441b32e91b9a904be8ea81f7a0afd14bad8ee7c8efc305ace5d3dd61b996febe8da4f56ca0919359a7533216e2999fc87ff7d8f176fbecb3d6f34278b", + "derive_key": "050df97f8c2ead654d9bb3ab8c9178edcd902a32f8495949feadcc1e0480c46b3604131bbd6e3ba573b6dd682fa0a63e5b165d39fc43a625d00207607a2bfeb65ff1d29292152e26b298868e3b87be95d6458f6f2ce6118437b632415abe6ad522874bcd79e4030a5e7bad2efa90a7a7c67e93f0a18fb28369d0a9329ab5c24134ccb0" + }, + { + "input_len": 3073, + "hash": "7124b49501012f81cc7f11ca069ec9226cecb8a2c850cfe644e327d22d3e1cd39a27ae3b79d68d89da9bf25bc27139ae65a324918a5f9b7828181e52cf373c84f35b639b7fccbb985b6f2fa56aea0c18f531203497b8bbd3a07ceb5926f1cab74d14bd66486d9a91eba99059a98bd1cd25876b2af5a76c3e9eed554ed72ea952b603bf", + "keyed_hash": "68dede9bef00ba89e43f31a6825f4cf433389fedae75c04ee9f0cf16a427c95a96d6da3fe985054d3478865be9a092250839a697bbda74e279e8a9e69f0025e4cfddd6cfb434b1cd9543aaf97c635d1b451a4386041e4bb100f5e45407cbbc24fa53ea2de3536ccb329e4eb9466ec37093a42cf62b82903c696a93a50b702c80f3c3c5", + "derive_key": "72613c9ec9ff7e40f8f5c173784c532ad852e827dba2bf85b2ab4b76f7079081576288e552647a9d86481c2cae75c2dd4e7c5195fb9ada1ef50e9c5098c249d743929191441301c69e1f48505a4305ec1778450ee48b8e69dc23a25960fe33070ea549119599760a8a2d28aeca06b8c5e9ba58bc19e11fe57b6ee98aa44b2a8e6b14a5" + }, + { + "input_len": 4096, + "hash": "015094013f57a5277b59d8475c0501042c0b642e531b0a1c8f58d2163229e9690289e9409ddb1b99768eafe1623da896faf7e1114bebeadc1be30829b6f8af707d85c298f4f0ff4d9438aef948335612ae921e76d411c3a9111df62d27eaf871959ae0062b5492a0feb98ef3ed4af277f5395172dbe5c311918ea0074ce0036454f620", + "keyed_hash": "befc660aea2f1718884cd8deb9902811d332f4fc4a38cf7c7300d597a081bfc0bbb64a36edb564e01e4b4aaf3b060092a6b838bea44afebd2deb8298fa562b7b597c757b9df4c911c3ca462e2ac89e9a787357aaf74c3b56d5c07bc93ce899568a3eb17d9250c20f6c5f6c1e792ec9a2dcb715398d5a6ec6d5c54f586a00403a1af1de", + "derive_key": "1e0d7f3db8c414c97c6307cbda6cd27ac3b030949da8e23be1a1a924ad2f25b9d78038f7b198596c6cc4a9ccf93223c08722d684f240ff6569075ed81591fd93f9fff1110b3a75bc67e426012e5588959cc5a4c192173a03c00731cf84544f65a2fb9378989f72e9694a6a394a8a30997c2e67f95a504e631cd2c5f55246024761b245" + }, + { + "input_len": 4097, + "hash": "9b4052b38f1c5fc8b1f9ff7ac7b27cd242487b3d890d15c96a1c25b8aa0fb99505f91b0b5600a11251652eacfa9497b31cd3c409ce2e45cfe6c0a016967316c426bd26f619eab5d70af9a418b845c608840390f361630bd497b1ab44019316357c61dbe091ce72fc16dc340ac3d6e009e050b3adac4b5b2c92e722cffdc46501531956", + "keyed_hash": "00df940cd36bb9fa7cbbc3556744e0dbc8191401afe70520ba292ee3ca80abbc606db4976cfdd266ae0abf667d9481831ff12e0caa268e7d3e57260c0824115a54ce595ccc897786d9dcbf495599cfd90157186a46ec800a6763f1c59e36197e9939e900809f7077c102f888caaf864b253bc41eea812656d46742e4ea42769f89b83f", + "derive_key": "aca51029626b55fda7117b42a7c211f8c6e9ba4fe5b7a8ca922f34299500ead8a897f66a400fed9198fd61dd2d58d382458e64e100128075fc54b860934e8de2e84170734b06e1d212a117100820dbc48292d148afa50567b8b84b1ec336ae10d40c8c975a624996e12de31abbe135d9d159375739c333798a80c64ae895e51e22f3ad" + }, + { + "input_len": 5120, + "hash": "9cadc15fed8b5d854562b26a9536d9707cadeda9b143978f319ab34230535833acc61c8fdc114a2010ce8038c853e121e1544985133fccdd0a2d507e8e615e611e9a0ba4f47915f49e53d721816a9198e8b30f12d20ec3689989175f1bf7a300eee0d9321fad8da232ece6efb8e9fd81b42ad161f6b9550a069e66b11b40487a5f5059", + "keyed_hash": "2c493e48e9b9bf31e0553a22b23503c0a3388f035cece68eb438d22fa1943e209b4dc9209cd80ce7c1f7c9a744658e7e288465717ae6e56d5463d4f80cdb2ef56495f6a4f5487f69749af0c34c2cdfa857f3056bf8d807336a14d7b89bf62bef2fb54f9af6a546f818dc1e98b9e07f8a5834da50fa28fb5874af91bf06020d1bf0120e", + "derive_key": "7a7acac8a02adcf3038d74cdd1d34527de8a0fcc0ee3399d1262397ce5817f6055d0cefd84d9d57fe792d65a278fd20384ac6c30fdb340092f1a74a92ace99c482b28f0fc0ef3b923e56ade20c6dba47e49227166251337d80a037e987ad3a7f728b5ab6dfafd6e2ab1bd583a95d9c895ba9c2422c24ea0f62961f0dca45cad47bfa0d" + }, + { + "input_len": 5121, + "hash": "628bd2cb2004694adaab7bbd778a25df25c47b9d4155a55f8fbd79f2fe154cff96adaab0613a6146cdaabe498c3a94e529d3fc1da2bd08edf54ed64d40dcd6777647eac51d8277d70219a9694334a68bc8f0f23e20b0ff70ada6f844542dfa32cd4204ca1846ef76d811cdb296f65e260227f477aa7aa008bac878f72257484f2b6c95", + "keyed_hash": "6ccf1c34753e7a044db80798ecd0782a8f76f33563accaddbfbb2e0ea4b2d0240d07e63f13667a8d1490e5e04f13eb617aea16a8c8a5aaed1ef6fbde1b0515e3c81050b361af6ead126032998290b563e3caddeaebfab592e155f2e161fb7cba939092133f23f9e65245e58ec23457b78a2e8a125588aad6e07d7f11a85b88d375b72d", + "derive_key": "b07f01e518e702f7ccb44a267e9e112d403a7b3f4883a47ffbed4b48339b3c341a0add0ac032ab5aaea1e4e5b004707ec5681ae0fcbe3796974c0b1cf31a194740c14519273eedaabec832e8a784b6e7cfc2c5952677e6c3f2c3914454082d7eb1ce1766ac7d75a4d3001fc89544dd46b5147382240d689bbbaefc359fb6ae30263165" + }, + { + "input_len": 6144, + "hash": "3e2e5b74e048f3add6d21faab3f83aa44d3b2278afb83b80b3c35164ebeca2054d742022da6fdda444ebc384b04a54c3ac5839b49da7d39f6d8a9db03deab32aade156c1c0311e9b3435cde0ddba0dce7b26a376cad121294b689193508dd63151603c6ddb866ad16c2ee41585d1633a2cea093bea714f4c5d6b903522045b20395c83", + "keyed_hash": "3d6b6d21281d0ade5b2b016ae4034c5dec10ca7e475f90f76eac7138e9bc8f1dc35754060091dc5caf3efabe0603c60f45e415bb3407db67e6beb3d11cf8e4f7907561f05dace0c15807f4b5f389c841eb114d81a82c02a00b57206b1d11fa6e803486b048a5ce87105a686dee041207e095323dfe172df73deb8c9532066d88f9da7e", + "derive_key": "2a95beae63ddce523762355cf4b9c1d8f131465780a391286a5d01abb5683a1597099e3c6488aab6c48f3c15dbe1942d21dbcdc12115d19a8b8465fb54e9053323a9178e4275647f1a9927f6439e52b7031a0b465c861a3fc531527f7758b2b888cf2f20582e9e2c593709c0a44f9c6e0f8b963994882ea4168827823eef1f64169fef" + }, + { + "input_len": 6145, + "hash": "f1323a8631446cc50536a9f705ee5cb619424d46887f3c376c695b70e0f0507f18a2cfdd73c6e39dd75ce7c1c6e3ef238fd54465f053b25d21044ccb2093beb015015532b108313b5829c3621ce324b8e14229091b7c93f32db2e4e63126a377d2a63a3597997d4f1cba59309cb4af240ba70cebff9a23d5e3ff0cdae2cfd54e070022", + "keyed_hash": "9ac301e9e39e45e3250a7e3b3df701aa0fb6889fbd80eeecf28dbc6300fbc539f3c184ca2f59780e27a576c1d1fb9772e99fd17881d02ac7dfd39675aca918453283ed8c3169085ef4a466b91c1649cc341dfdee60e32231fc34c9c4e0b9a2ba87ca8f372589c744c15fd6f985eec15e98136f25beeb4b13c4e43dc84abcc79cd4646c", + "derive_key": "379bcc61d0051dd489f686c13de00d5b14c505245103dc040d9e4dd1facab8e5114493d029bdbd295aaa744a59e31f35c7f52dba9c3642f773dd0b4262a9980a2aef811697e1305d37ba9d8b6d850ef07fe41108993180cf779aeece363704c76483458603bbeeb693cffbbe5588d1f3535dcad888893e53d977424bb707201569a8d2" + }, + { + "input_len": 7168, + "hash": "61da957ec2499a95d6b8023e2b0e604ec7f6b50e80a9678b89d2628e99ada77a5707c321c83361793b9af62a40f43b523df1c8633cecb4cd14d00bdc79c78fca5165b863893f6d38b02ff7236c5a9a8ad2dba87d24c547cab046c29fc5bc1ed142e1de4763613bb162a5a538e6ef05ed05199d751f9eb58d332791b8d73fb74e4fce95", + "keyed_hash": "b42835e40e9d4a7f42ad8cc04f85a963a76e18198377ed84adddeaecacc6f3fca2f01d5277d69bb681c70fa8d36094f73ec06e452c80d2ff2257ed82e7ba348400989a65ee8daa7094ae0933e3d2210ac6395c4af24f91c2b590ef87d7788d7066ea3eaebca4c08a4f14b9a27644f99084c3543711b64a070b94f2c9d1d8a90d035d52", + "derive_key": "11c37a112765370c94a51415d0d651190c288566e295d505defdad895dae223730d5a5175a38841693020669c7638f40b9bc1f9f39cf98bda7a5b54ae24218a800a2116b34665aa95d846d97ea988bfcb53dd9c055d588fa21ba78996776ea6c40bc428b53c62b5f3ccf200f647a5aae8067f0ea1976391fcc72af1945100e2a6dcb88" + }, + { + "input_len": 7169, + "hash": "a003fc7a51754a9b3c7fae0367ab3d782dccf28855a03d435f8cfe74605e781798a8b20534be1ca9eb2ae2df3fae2ea60e48c6fb0b850b1385b5de0fe460dbe9d9f9b0d8db4435da75c601156df9d047f4ede008732eb17adc05d96180f8a73548522840779e6062d643b79478a6e8dbce68927f36ebf676ffa7d72d5f68f050b119c8", + "keyed_hash": "ed9b1a922c046fdb3d423ae34e143b05ca1bf28b710432857bf738bcedbfa5113c9e28d72fcbfc020814ce3f5d4fc867f01c8f5b6caf305b3ea8a8ba2da3ab69fabcb438f19ff11f5378ad4484d75c478de425fb8e6ee809b54eec9bdb184315dc856617c09f5340451bf42fd3270a7b0b6566169f242e533777604c118a6358250f54", + "derive_key": "554b0a5efea9ef183f2f9b931b7497995d9eb26f5c5c6dad2b97d62fc5ac31d99b20652c016d88ba2a611bbd761668d5eda3e568e940faae24b0d9991c3bd25a65f770b89fdcadabcb3d1a9c1cb63e69721cacf1ae69fefdcef1e3ef41bc5312ccc17222199e47a26552c6adc460cf47a72319cb5039369d0060eaea59d6c65130f1dd" + }, + { + "input_len": 8192, + "hash": "aae792484c8efe4f19e2ca7d371d8c467ffb10748d8a5a1ae579948f718a2a635fe51a27db045a567c1ad51be5aa34c01c6651c4d9b5b5ac5d0fd58cf18dd61a47778566b797a8c67df7b1d60b97b19288d2d877bb2df417ace009dcb0241ca1257d62712b6a4043b4ff33f690d849da91ea3bf711ed583cb7b7a7da2839ba71309bbf", + "keyed_hash": "dc9637c8845a770b4cbf76b8daec0eebf7dc2eac11498517f08d44c8fc00d58a4834464159dcbc12a0ba0c6d6eb41bac0ed6585cabfe0aca36a375e6c5480c22afdc40785c170f5a6b8a1107dbee282318d00d915ac9ed1143ad40765ec120042ee121cd2baa36250c618adaf9e27260fda2f94dea8fb6f08c04f8f10c78292aa46102", + "derive_key": "ad01d7ae4ad059b0d33baa3c01319dcf8088094d0359e5fd45d6aeaa8b2d0c3d4c9e58958553513b67f84f8eac653aeeb02ae1d5672dcecf91cd9985a0e67f4501910ecba25555395427ccc7241d70dc21c190e2aadee875e5aae6bf1912837e53411dabf7a56cbf8e4fb780432b0d7fe6cec45024a0788cf5874616407757e9e6bef7" + }, + { + "input_len": 8193, + "hash": "bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3bb2282aa69be089359ea1154b9a9286c4a56af4de975a9aa4a5c497654914d279bea60bb6d2cf7225a2fa0ff5ef56bbe4b149f3ed15860f78b4e2ad04e158e375c1e0c0b551cd7dfc82f1b155c11b6b3ed51ec9edb30d133653bb5709d1dbd55f4e1ff6", + "keyed_hash": "954a2a75420c8d6547e3ba5b98d963e6fa6491addc8c023189cc519821b4a1f5f03228648fd983aef045c2fa8290934b0866b615f585149587dda2299039965328835a2b18f1d63b7e300fc76ff260b571839fe44876a4eae66cbac8c67694411ed7e09df51068a22c6e67d6d3dd2cca8ff12e3275384006c80f4db68023f24eebba57", + "derive_key": "af1e0346e389b17c23200270a64aa4e1ead98c61695d917de7d5b00491c9b0f12f20a01d6d622edf3de026a4db4e4526225debb93c1237934d71c7340bb5916158cbdafe9ac3225476b6ab57a12357db3abbad7a26c6e66290e44034fb08a20a8d0ec264f309994d2810c49cfba6989d7abb095897459f5425adb48aba07c5fb3c83c0" + }, + { + "input_len": 16384, + "hash": "f875d6646de28985646f34ee13be9a576fd515f76b5b0a26bb324735041ddde49d764c270176e53e97bdffa58d549073f2c660be0e81293767ed4e4929f9ad34bbb39a529334c57c4a381ffd2a6d4bfdbf1482651b172aa883cc13408fa67758a3e47503f93f87720a3177325f7823251b85275f64636a8f1d599c2e49722f42e93893", + "keyed_hash": "9e9fc4eb7cf081ea7c47d1807790ed211bfec56aa25bb7037784c13c4b707b0df9e601b101e4cf63a404dfe50f2e1865bb12edc8fca166579ce0c70dba5a5c0fc960ad6f3772183416a00bd29d4c6e651ea7620bb100c9449858bf14e1ddc9ecd35725581ca5b9160de04060045993d972571c3e8f71e9d0496bfa744656861b169d65", + "derive_key": "160e18b5878cd0df1c3af85eb25a0db5344d43a6fbd7a8ef4ed98d0714c3f7e160dc0b1f09caa35f2f417b9ef309dfe5ebd67f4c9507995a531374d099cf8ae317542e885ec6f589378864d3ea98716b3bbb65ef4ab5e0ab5bb298a501f19a41ec19af84a5e6b428ecd813b1a47ed91c9657c3fba11c406bc316768b58f6802c9e9b57" + }, + { + "input_len": 31744, + "hash": "62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47860cc51f2b0c28a7b77304bd55fe73af663c02d3f52ea053ba43431ca5bab7bfea2f5e9d7121770d88f70ae9649ea713087d1914f7f312147e247f87eb2d4ffef0ac978bf7b6579d57d533355aa20b8b77b13fd09748728a5cc327a8ec470f4013226f", + "keyed_hash": "efa53b389ab67c593dba624d898d0f7353ab99e4ac9d42302ee64cbf9939a4193a7258db2d9cd32a7a3ecfce46144114b15c2fcb68a618a976bd74515d47be08b628be420b5e830fade7c080e351a076fbc38641ad80c736c8a18fe3c66ce12f95c61c2462a9770d60d0f77115bbcd3782b593016a4e728d4c06cee4505cb0c08a42ec", + "derive_key": "39772aef80e0ebe60596361e45b061e8f417429d529171b6764468c22928e28e9759adeb797a3fbf771b1bcea30150a020e317982bf0d6e7d14dd9f064bc11025c25f31e81bd78a921db0174f03dd481d30e93fd8e90f8b2fee209f849f2d2a52f31719a490fb0ba7aea1e09814ee912eba111a9fde9d5c274185f7bae8ba85d300a2b" + }, + { + "input_len": 102400, + "hash": "bc3e3d41a1146b069abffad3c0d44860cf664390afce4d9661f7902e7943e085e01c59dab908c04c3342b816941a26d69c2605ebee5ec5291cc55e15b76146e6745f0601156c3596cb75065a9c57f35585a52e1ac70f69131c23d611ce11ee4ab1ec2c009012d236648e77be9295dd0426f29b764d65de58eb7d01dd42248204f45f8e", + "keyed_hash": "1c35d1a5811083fd7119f5d5d1ba027b4d01c0c6c49fb6ff2cf75393ea5db4a7f9dbdd3e1d81dcbca3ba241bb18760f207710b751846faaeb9dff8262710999a59b2aa1aca298a032d94eacfadf1aa192418eb54808db23b56e34213266aa08499a16b354f018fc4967d05f8b9d2ad87a7278337be9693fc638a3bfdbe314574ee6fc4", + "derive_key": "4652cff7a3f385a6103b5c260fc1593e13c778dbe608efb092fe7ee69df6e9c6d83a3e041bc3a48df2879f4a0a3ed40e7c961c73eff740f3117a0504c2dff4786d44fb17f1549eb0ba585e40ec29bf7732f0b7e286ff8acddc4cb1e23b87ff5d824a986458dcc6a04ac83969b80637562953df51ed1a7e90a7926924d2763778be8560" + } + ], + "random": [ + {"seed": 1, "len": 0, "xof": 16, "key": "4e2873a644ab37671ab25c9962c09661f89625981a15d94c02b696cb27ab6110", "ctx": "lambda-vm oracle review ctx 0/16", "hash": "af1349b9f5f9a1a6a0404dea36dcc949", "keyed": "e2060fc733e3b0c2b258652b301a876b", "derive": "18592c7bb4d1ffeaa4c1a65c9033c1ed"}, + {"seed": 2, "len": 0, "xof": 32, "key": "51f2f6c1699b22240a722f58d8f4e1d847dab43424ab9c7f6cb007e12d4f5575", "ctx": "lambda-vm oracle review ctx 0/32", "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", "keyed": "f53925740cc09cd9a37b1d00920479d275d847e0279c49a1b65001943158da28", "derive": "4624d0bf48875f0faa7f0a14e97b564152d1315d6f3777ff5c8bc4fa7df5b51e"}, + {"seed": 3, "len": 0, "xof": 64, "key": "772fa66e71671508c8633f754cdc205bc7ab06a0b8b0b056d1cb550195ec194b", "ctx": "lambda-vm oracle review ctx 0/64", "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a", "keyed": "69d6a0893237ef913262d2dc8e2c71f8d6e8346c3745e7f622898791993de30b227fd4e9500b07b0e42b0f7978f7bc668cf0053f27d993bd3b85dd0567fe3549", "derive": "61aa6ed699d26c31cc82858c9df151de16f5459e752b62d1829cdf00dfb555a4c4240102469bc7e1076871b76c1276734dda1f5aa7147b7c7d8613f1cb763001"}, + {"seed": 4, "len": 0, "xof": 131, "key": "0876f38380a77686e29a0c55b4c16d1d07ae429f299d141e3245cc47f35b9d6f", "ctx": "lambda-vm oracle review ctx 0/131", "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d", "keyed": "a5a703baaa710a23e4386586538ff90cffba019e51751a27d98a69e15c372232737faadb85d92412e653759d94aa5abd818b75a9b5108a252ccfdd8b71d92cb9e96b3f52777a355f36d69bc2c495975f889ca448bcf3e099b7940d7fe67038bf2004c04bcd164664285bd69852fbbca42ec60b44adcec2e4ddaea9cb1506320a577b9a", "derive": "a82dfed98f8522164b49badbafa145d39e734f9e868a3c0815b79df69affa65ba9ba052ac0d15b4021676f99dbfb8990a568b77c2c19cbfb5e6fefa1659c08f856b60f8311c676107d56120f9693ee995002dcc4b5e90c4bfdfe7ecbef71e0c40422c015ed6fe5e57432dc6b8c85ffa34b45e43bf94d075042041e47e963e235ea407e"}, + {"seed": 5, "len": 0, "xof": 200, "key": "9d0ff0dd6e45b03425a5b93d7f2f78b817bb8647ea6dfd057fc219ea00d1e2fe", "ctx": "lambda-vm oracle review ctx 0/200", "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d30f8a03e49ee25d2ea3cd48a568957b378a65af65fc35fb3e9e12b81ca2d82cdee16c68908a6772f827564336933c89e6908b2f9c7d1811c0eb795cbd5898fe6f5e8af7633", "keyed": "58265ba18bfcdfa1863b0adc953d516e4a3cec2e2d09c4de65fff6d5ee7cb0b9bbf98267b3435d95521c20397e729b3c0094e9c3bfde552931c3451764b6574c7bd0d058b9cecd209a54df2da39a6160729b8e7df79395d5b48204055973b27f5f1ba83971fde2fdad3e6bbe2729eb3e76859edda23f577ba277546934bfe03e3351349cdcd05c71b04677852ab7cd0992c5faed20eb878fbfd374f5328a92bc8736dce061f787f0a9f435d8d9eab7e1ee5245a2e98a556917d2fa2b996b7ff267965f883cdc73de", "derive": "c49da0d52d5230577d675dd17125b7c9d12ee8ecd2dd6b5c7a3bb87e31249b0817a489137718c47e36f1f55fde71db1f47ea3a16c0e53d7e8f61778a199b664d88d598dc0464883110d1898a53f32c4e72728f774bdf5b949c984d645fc48cffd6fc8b867b36b8c568d91cadc0207aff6677ee79f0ca46b34aba8fbbb4d7caead47c39c50468d51c10a9e2d0a21f95360fdd067f234395e488b3a8110faa3c2d791c3bbb836c820a1f27bf8add1994ba93895fee5766cbd6e6524308800bd6763a18ce98b7716faf"}, + {"seed": 6, "len": 1, "xof": 16, "key": "317c727c3588b8406115f201e6afb5cd7abdc51490aaec9c29278c93d48ab42e", "ctx": "lambda-vm oracle review ctx 1/16", "hash": "f633b185e996eb1973f7b5e81da3add1", "keyed": "2cf20a61154aa7b99ec1e808d64504d3", "derive": "031fac35c559618b2d8df9ba8329cbb5"}, + {"seed": 7, "len": 1, "xof": 32, "key": "c515bba50da1bdc09d42110712b6094ae75f9ad50bc151f1efa27b7bf6a4483b", "ctx": "lambda-vm oracle review ctx 1/32", "hash": "ad821f67cec320b5cabc58516eb59f00ffc47e0a23db617879a2817b7560d081", "keyed": "5c20577f80e9d2525dbf76f370eae9b6f2e040990b06323106ceb3f4c59ca77f", "derive": "ab7326c844abcfd063239f0b96a23b1adec9eb82b0a0a5bff421bdc0567a8813"}, + {"seed": 8, "len": 1, "xof": 64, "key": "c54c0ceab664949da493637296c6e82ccb0cbcd3c0fab16029882933ca84927c", "ctx": "lambda-vm oracle review ctx 1/64", "hash": "ad821f67cec320b5cabc58516eb59f00ffc47e0a23db617879a2817b7560d08140f3fdb8e19afa84cf36653dab48871eee1ae3548a75bff74a0f7268bca72569", "keyed": "4c4897a7500db1cd436be752b3186744701743822d6e4926f7cf662b56749f49d2c4569660f7ca12bbfa1483e42df23f21c90d93fd563010e196bf2f19f01871", "derive": "e7696f2d701338d5725eea71e792aef3fe724ee2e257feedb59123334f8dfaa07d57460273286218154b7722ee7d6a6b2efe055ec942df34eabb325afe39bb42"}, + {"seed": 9, "len": 1, "xof": 131, "key": "eb899ae51ef9827c78b6d1b224ac020a475fcd4387a2dc0afb5ce436c256acbe", "ctx": "lambda-vm oracle review ctx 1/131", "hash": "2ca3a1b761c6412251d07b8a707fb85e306530bc393bf47664172bf5078e278177f5fa73496da85a1f43bcbab552dd7bd29627ef86ef7aa1af9499a15a1bf681c3a273bb00421538827f685d575e2f6040047674fd005d3c3a90e83b34a72f8c12b528c1c7ae262c79b5f9e51c66344eddd3b0f5a1387233bd10247c302f6de304d598", "keyed": "f80116f4d31674dd65a0cca63b2ad7ce5e07c25ab4936eab9532422760778ec7bd59719681aabdddb89e23855cb7c8b28536dd7776d22cfb840531734b1aba68aa70b2ef0e485e729618b12fdf9354081a1dcfeb29ed6e65daf5cc9e46e68ac025c7b4726ddaeebcd3aa8c3ef22190322d350729210c51e5bda090c9c2bc985aac4d04", "derive": "691c85c36007c7270f0608ede75a83818afae06b550aec1698ea81935db7657a35577fb9bd3121aab6c952a0367a3603d8e79072c07ed3796cb0a2e926677a06757d7690d3f66ca7ea5b805abba98c17646c6fe6f1894d4bd0f4519c5c7cf8a9cf2cf6672041432d20da197c01169972d47bd8e3f8bdb34e33d2fe89369964d5aee5ae"}, + {"seed": 10, "len": 1, "xof": 200, "key": "119a6021250e721aa23d6096a7e6f8e9e9de5cc085ec7540afbb588f84476632", "ctx": "lambda-vm oracle review ctx 1/200", "hash": "f17067d6d29eb1bb0208ce84d40ee47504c8656b67e7b663eb1d7182e090b0f4399fdbd2be2d9455eac4e40814d8e5b6ed8fd9090ba9c067a17c959a604ae5501bded22884e9617c71ea944374ce143bb040d8cf377b9a09046e9c769710e755940120d137f08a598f4eed9f69f3961ee13b1f25a661dabde0c6101b3cc7b492a72a9e83f4fb44adc30a86f8124b351dc18d9b7c0c711d3a49ca2b5eee3a341e45970d9981692df5b6b65103f9bc270c1733d30d815ca09b8cf134aef4c8bced5da99519932fdb51", "keyed": "2b3b662e366d5b27c2abfdac897acca87a74bfb3eaa3c44a23bec82ad6c923b80f799d1385fa77cb270689f7a9fde16d6f335b76dc4b16b5457377736ee78c26bd0cd9bbab04f8513bbc98709ab3bf5641d4f6de572607748ddde857df6532cafa4075aae6e0afd5940c62af20e71f78eb8aa4eaa25386cb2aa6a6e619fca07226715e4001230dce1ca4c74365273a2248fbb65decd69337fae6578474bb3a0d04e347f930b5ee431c64ed9b528143becf0e6da9c78a3afba6183a08d85877a6a64a79055738c96d", "derive": "566ebbc1e00fd71b6f35665b60075c8238bf87dd3bb4f58a0b7e94b6599a37a983f27b9274b35c9187160e010a70cd2b3562ddc6b779b1d3ff177114e8a22c827649fcbb26db2a6feddd5fc135c057c3c9e4ab63bb04d57bfd89c3ddec14b7a85cb3b12951ff8c5a0ab7bc983854787bad61ebba20e33f85f79cf8f49c0fada60b62c412e8cae0c2f75e374f24c5a41f274ce041a7379881517c69a72dd631a94ea72dd82ef1c3721004db70a1685330337a30da3678427d1aaaffe3a440f2f6b6b9a5fe912511b3"}, + {"seed": 11, "len": 2, "xof": 16, "key": "36d7a2702afdc652e3931fffedc5e1aaea8215f349f940ebd1eb941dd5e47318", "ctx": "lambda-vm oracle review ctx 2/16", "hash": "0274374489ae986e5475921ba458907b", "keyed": "54ff07b6a6acd7a1f6b0961b318bb08c", "derive": "09ad6852b188894de999199fb6fb74a2"}, + {"seed": 12, "len": 2, "xof": 32, "key": "a5d7f0a5d770f63da9dbd45bed6874726ed533458db65685690376ab47d6fa82", "ctx": "lambda-vm oracle review ctx 2/32", "hash": "ae2ed86f46c2a7fe7d0f242aa6aac5996059df9533838c17c786c04b9ca0f644", "keyed": "de176d34225fe68f469150df8f5ba8cd61bc4a03872962e312b8dda7e83669e2", "derive": "30dfa512613e64ea073c79b3bfde819c269dee858a1af35a9ebe53540098c6c4"}, + {"seed": 13, "len": 2, "xof": 64, "key": "39705c1c83976fb01c0ac14b1f5c5170b5ed292906fe65e483ea6410ac670871", "ctx": "lambda-vm oracle review ctx 2/64", "hash": "c91ff7a53e49408b407f10d8f9c8ed080b5de4811502f0d5f9c5096fb57df4f01b5b2fcda59f9150970abbd8e5bf0f30b5658b5d236b66a70e26a566f77a88c1", "keyed": "5a61e26a1347ee4e3712cdeef297b4a435a896cab11ca069195291f401412a26dbaaf4e03fd66a258bd656f094b4deb643342edf6eb2d237cc5e5c780acdea9a", "derive": "2e6195ec1ba4d7d8f198e6d5e55690bacb567b00bb3c0fee39103463f4353e7b85fd3d70172158b7053da07500906d6d5c3e74e661c46d364f096064bda855f3"}, + {"seed": 14, "len": 2, "xof": 131, "key": "f024acdc8f5356491b8fad84636e34b608fddaac3f4b5c0c67580d0ddd2766a8", "ctx": "lambda-vm oracle review ctx 2/131", "hash": "f997bcf0a91417c7a4a7eedf735d986be1b183d455057a5655fd77147c0db0396da06133facf9dad5523ccb852b43e531f415c4bf0aa170fa9407b1e6667d4e0e3bb59ab2ffabbedcf060af9bda300dcc6fcad1b223c6717b574a1b512777193a9ea7684ee725fa6a93210b782b128d2aecce569918501e61ccc664487174dbf6c390c", "keyed": "0a4090e3375db9ad9dcb59055a53cee9a40b48a7c6f85cb6de6e4b77d626e2540bf035a7e9363b04e0595d82107d0ad723686b76de610d2b32d7155e1874af4763a917b61a47067b0c25461f9a4896fcb1c660ead2410858b7a29aa4ccfdaf49ed28f505085121acf3d75cba353be74a598a8e81560f9e8e7174ed6c84a3f623ff3dbd", "derive": "0a8f4b6d80548fba6628b0f9342e27fee4be72a5498b6fecb46ac4a45b5e320cf94ddaf8c7a4131b6e5b46183cf08dd2ae5310f75dace83b9c5af6d7ce7041e01f40ed2afaf4a38e79409cb8203a5d75301d40a99a45f3f0289757874ef5df2a23ba941b3ba23bd70047c5148bdd45ef502e598a8c3ca0390756fd332cea76d1ccd0da"}, + {"seed": 15, "len": 2, "xof": 200, "key": "85bd87a61d3b05fcfd17c2258ad1a40b37396d6aa816e1127500d5d5928a0a90", "ctx": "lambda-vm oracle review ctx 2/200", "hash": "2c9c567961e877f29cf5469d28f8b1341295df42fb56e5e2c3c240bd63c57778f4bdcde180ae4be83337936fd51f01349e5356b15319852c5b50dffd1c099c34090283b352e8d350263103684047399418cc319d770105f23ca00634cc733f06e320db36d68954c6d44fc6b5853462ea1019a9941fb1041cfc00d590c1c4e487569ad44acac818b9b3ee7d406c736f41f70f72cee3b133b2241a3fb8627a0fe3fa92341a6b6fc5e1222aa688f49d936b6b2ec99cfd1fde1fe6611fdccdce74efaf54a9cabe220723", "keyed": "024283d0b8224be9032765f2cd1076dfc7f4cf6cb947a9c7367ffb4e082cdf137522dad8ec833a35c211797431e9de7d7ddd3fc35692b7cc943400095e978db894b9bc37e1ca7ed271d1edfc0befa95a64a39e71c34b46d4d557e3770f8f01b7c8fa7349ffec6e593e10af3d7bd713c8faef0b00272b347a7baa2a135f006c5f3efdca2ab53b73532f30eb377ee52f9833a556cf76a90e51da5c5e9b42666e09788d4308103f19d596fd961255801a65ce10c85735830e039830ffc36da06ef54eb17dbf3d99e3c2", "derive": "600c0981573e4afba64240a4246475ee49f8b73fd8c0f89a122077676c63e59ec6633c5ce0b851122fb91bbee915b6c929ec653b651986c0a5f9b75b421f7e2813c46f885bf203ff25e511cbaa904d96feb55c336ca224ba11b1bac173072dc69fe3e90fab5bf5b11819479652c5afe014a2e42406191ee4c456a03f7ff7c06ff77c4c9f9569592500ac9f1a5e1491080c163e76e9f4ecd849f6e2818e97db2cf67c7d616d801717002173f4f331da718840597c7b381d1e164a789169ac6c26ea34a470f8692bfa"}, + {"seed": 16, "len": 31, "xof": 16, "key": "a7f5e028fe27073413baf3c095354912dfd380d484e9cfeb548c1492fdc5bbf6", "ctx": "lambda-vm oracle review ctx 31/16", "hash": "e45042ca397670d5d2ef5eb5be0783fa", "keyed": "a5a9e20141df86780d1b27f11b0d1bd4", "derive": "a3bcef201e7ab8174a69e1a97ea66de5"}, + {"seed": 17, "len": 31, "xof": 32, "key": "cda155913dc188c34335e58f73091fe30cce6cf755a62f967c0837a51ddfb283", "ctx": "lambda-vm oracle review ctx 31/32", "hash": "b014f91e00ef5410cfef91b1bd8efd88d398ea2ceaa8a6522ce4157d6d57a5fb", "keyed": "61bad733c76597cf015c4bf9bf6bc766f06369fd79a6b42232946be96b84f719", "derive": "56ca83cbed45c5e4cf5925a22493de95b252ece7d578655be136a03985e2cc96"}, + {"seed": 18, "len": 31, "xof": 64, "key": "d0fca40ee0a6fbbb65f2e91e27643c0d4141d20b6aae333f64ba2fc4661360cd", "ctx": "lambda-vm oracle review ctx 31/64", "hash": "3a9668075cf2350c98e361bf3138f6de927f0c618974a38df8787c440a10fc6e6839bd4613297bf68e1f591f24087f4841a50e79309f96b17b132dd0e1ad627f", "keyed": "b6bde96e47e786800e0f501a77babd92291666f168d05a23cbe57cc7f6384d4e58ec2199f0bc02cb719632403663483a409986f48caef7f1692d5e32668b52e0", "derive": "01e6cfde455c562b5d41ba800be896c0bde376fa3803ce37bce821b92bbfc7e1a08b375bb84eeb5a2c3300dd667ee9212d441cd382b73b97fe0b2172aaf3499a"}, + {"seed": 19, "len": 31, "xof": 131, "key": "f6a765475e789f2922e039484a8f8b1d9da6328f67701ec6ae99c800b5f72b61", "ctx": "lambda-vm oracle review ctx 31/131", "hash": "c42704dfc6c9013843886334233782451a5f5fbb8fccbb8e1b6ce89f3c2c8d82674dc0555a7313e23254a13c195da6c8c36d5adff3ff1da19a739a78efbf324b9d32017bfeb744676230118e5f7714cf22b5ad0eb494bf3b60f67e788b11cb9cba815c3a79a60314ddc7c8be71d6febace72a99b1bb6524446fe6e24735335dfd4de41", "keyed": "ebc1ca503e3ff98fe60319c9464e35b592b1169515178d01a7a4a09b9cdf58d041cd589b7d91438b01cf16453b03917b9ff4f33a9027c6e0fda2d37f526942120908541d46f232981f2927e26bcd9a6a60382ad2f09ffc581331910c02176428c85439817e96262eba404a76705e60cb689c9676779b4945bd25a485f7332951a956a6", "derive": "651fe4214293835fc5aa7353337d3dbee3f9f8f674271c403d41e4f22168a427cb60094adaec81f5f811d54c7facf79d23b7c8090c364d6f25da9263d57b053422c7f6d4524aaeb1e064b5a13ecb76df59e74a28c3af83980717f9c50f4ac1969b57e6d397d3b4f0e36acb2221d3d82779866b9830dbfa0c98968519f5d8ee9f2e7d21"}, + {"seed": 20, "len": 31, "xof": 200, "key": "427d2be2a0a273bcbd83a71c6356ae4db1e6dae70d4ceb93f8d3ceb5b3c2b0f4", "ctx": "lambda-vm oracle review ctx 31/200", "hash": "f2c450b25acf5d07c0eca77f91565db7b1724e825cec605ffa4c3811108a19ce7ab9bedcad7e970e2bdb188f807fcc8d176d7af48528a57f7f7e97b4cf480db33418b8faa8e9e0ec76fa02adafb7751e5ebec1d9a088e7234b3976ca652b2a58fb50402ad4db627b4a3f087295ecb180d75a2ea2cbb99aca5ff1cb01ae0faed7a17692f03d13454834f14247437a9a00749b68b17c633ceacbbf1ed72edea9321d3e3edb41a76acf21dc406224524a1ede3bd7f535bbce38fdf3429798a341245eea76c3d9451fe2", "keyed": "0279a7640de3adf0825a3840dd78c65a80162306b133ee52bc207b2b7bf639959a6fc394a2e23e79ce0c7cc44569aad0d3d24b3c39c010d5839744a86fe1f47a1b55e4dcd2898d13aecc7e9223010097e5ccb05cbaeb2b258c8785fb1d2edbdbbb827c9c5e2592ad901028a5cb5cfda8215acd4f36c03088f8c0a976f410991cc499ea5da7587c671a17d200949e6e801a6f1e8ba9e9bc6e704313f9f577b5a649f94c75ab983708388e232cbaba93097fba3378046efec08279025967db8540dd0470d24c5eee3a", "derive": "03a483217460e212f6c9454f0d3a0e0b0ca69d4e0bc53ad07724d66565598b82cc6bbaf8456c3d9ff6eb092ab38ba6308c82717daefe30ee3a963a531e6006f550d72bdf9335fd81e98dc5ba2e694b34b4a5db8f13f015f9c0a0f7bca527e2e164635dd3503026b2060d6d1ee44f887d5c3debbab2189b285585801d85a5af8b0374e58bf839ef6eed1c6a912d57983ebd5292af99b15f1e4f08a5c8cce02b625e729f5a5a1130a1f78de0bc3da9860349f4825963906b01c49ed7fed54bee7ed8af418d109d6acd"}, + {"seed": 21, "len": 32, "xof": 16, "key": "d6a839c8611a7bd19c9791970aa61e9453a6ce5014f54c84af38377d711ce1d7", "ctx": "lambda-vm oracle review ctx 32/16", "hash": "1f8db1bfdd8dd100a84848f8824b888f", "keyed": "0bcb7116c7635074d6df75bf5d5c932f", "derive": "c3191d21509721b6885d7498e86005c7"}, + {"seed": 22, "len": 32, "xof": 32, "key": "6b8488c931ef0c80a7a15aa039a2a4da72e5fd97686f344c037c08c151a7ee29", "ctx": "lambda-vm oracle review ctx 32/32", "hash": "39eb5d3bf52d1c4855e9e7475cb795c9eda725f55f8e77cc5e97decb3a0bb2b7", "keyed": "cc5dfd72264e95db8a511bc890e2339bfe3c8330b368fbc5208b33f072a9e0a1", "derive": "659185a1756867f59590deba74d025854eb6b6d3d2a61ee8340ea9efea42cb09"}, + {"seed": 23, "len": 32, "xof": 64, "key": "ffae277d623842753e96b906f13377ec53f8218c863a7ffe52a5805ac20c9468", "ctx": "lambda-vm oracle review ctx 32/64", "hash": "ddf820aa0395b67040adf9de32bfa880bd7bd6a63776c5834309436e331523aa3bbab766ae85510377fce8259d0b5037bae8954b037f0faf9136d5858053f1af", "keyed": "108fb30d7a4ce089e673a206df2ba91600b992d5ba40f04fdbc0a42b99e6e1dfced81ddccb58738b92037a6903d2d669c9a8bb8880e45302f38824fc7aa3177d", "derive": "b14a08b72587ef6f8e95ad7c39c02ad3f635da206d80dae709fe6a215f0f1a5e6d897e6a0d6666a380bc4896dcc9eba9f711845f921c5a745eefcb16a88b9566"}, + {"seed": 24, "len": 32, "xof": 131, "key": "445637047e96984af1f65f8d683deeb28858305868dcd5da010644a281f75396", "ctx": "lambda-vm oracle review ctx 32/131", "hash": "04153ec8aae42ce4e967b24e4f0fcea47b0b5bc6ff27ce555d0e29ad1afb044681191753eb69bf3c246d04bb83f0c5fa6c2b169cc1f84188a72c52e28b86c953f9bcfc0428c49c4a9ef3ac2d6d9f372ba83f8fa4bbcf568b38c05d4e0baf9f8cc5a1b915b8f86f1eb8baf428b79bf4eb24eda5831a105e0cd7b9af8954d5342b4d259c", "keyed": "b78d7c2f73e8bb10ae2d6ba2af88034e071497f362978f2c6dfb41038c2ae7cc8466a2e599237145b1117907fc23c9f74b3f9d7cc81e8c75cf849d31c24d88e256feb79810ca0c90a07c066b95464a0ca3acf5848ad903883c4cbc31c033f6e71b0ea152433769feb322503af0700be9833e8b8101d4ab0bd7302880ff4df965e442c5", "derive": "33a9f9cb60dae21962da63ee1d6926ce0bf1292e6f03760396d5b292c295843b13ff19472fe964caddbe09559f9840d1609ad0a3be1ba5a4b6c0c4688585ecb1a399beef3d4bca758fd94f96e0aba1c86d815fb5ea72a7d03a29b7abc2459a77aaaf8db735dc80abade3181de8bd9ad2a0f4b7d155b49e8300666252337b4aa37c975f"}, + {"seed": 25, "len": 32, "xof": 200, "key": "6a021b8bc4caeed9bf9bdca81c38ab88786e351821c08d22314b361187bc7318", "ctx": "lambda-vm oracle review ctx 32/200", "hash": "e04c6f90e7c7c64dc4482711d6fc69dfd0311807c5de185abdc58bff4e5dc75c950ca3397b856cec6a773e31ea95470bf2f8d16072142c35104d90537e6644d191387ee0250d617def6ac698c20324cb1a7ad4ee0ca9a72a0be13478341383041ef3855c6871ca02f92c0a30ef98748200d1ff5c483f28068325d80880b37ec3050587fb66b3bd40cd84c701c163b0fd33a22a26c7fddf7833e7012f0d282613e263ffe5f93ba2997829ad0b6583e8973cd656e531ffc34418bb5c454ef1571a8d719ce4aa9dcbdc", "keyed": "3ee76706e52a25d0b11fda2008594cb7c0d108e0a30a799685e68a36fc4d576a872dae2c86182c06f744cb84088296f26397726455a9cd3076027851f94cb2db376daa3f2fb478f3df351487f0d55bb9b789f3481496894af5f7295e76c9144bd6db381d89aa984f0d0f1fee767c61899809979057129f8395ed90a6eaf53b910629dd38026013769f2a0761681449cbe68e03e7e87201bf02b68f1f89397bab16da8b38dbd50278618fdc4dae10946246fa6cca05264bd6f4333921e2fbeca60bba45240b2e248a", "derive": "f8ce88a2f6f80dacafd922474d734c038d7eb0e6bf14a82055d64f14343336c88bebf482471aa7d0b4119d6bbe762b4d9f6284ceb83acb215e575f14e0ec4331b1e5337f0b78f5aeb683830b8337df041c0270aea9d7afb7fc04875a0ec985e16a7b2a60473a3241a7aa404a713b86061d8f87d20135a29294ff9cf4137175064834410219d6d8cfa009f9f866d4560488eb5933925cd15a4461b25a9a2f002c02a39c2e3a7c0b0517ffca563c1398aafb02c7a786376321a38d09b8ae68491b5f2d1c43db26d2a9"}, + {"seed": 26, "len": 33, "xof": 16, "key": "90a4ae28a4be801aad16678441130895919436864549da4392404ce5abe7f28b", "ctx": "lambda-vm oracle review ctx 33/16", "hash": "ef56ebde8209e7f40467b2a34930f5d5", "keyed": "3cf2f08202f6d76e0ca02fdbf2cc3f19", "derive": "0fc74c6095da0ff7afbddb487f526fa5"}, + {"seed": 27, "len": 33, "xof": 32, "key": "b54f00030d216c7e1db895eecafc89352606995fd98cbbb88426abdea6c0e38f", "ctx": "lambda-vm oracle review ctx 33/32", "hash": "61472133b26ffd46ed8a0321fc278559d720a5ed6c43a55bd771bf08aacf247a", "keyed": "3df5ec7583584487ab47307fc118aef9bbcc697193ca78ce4d010a1537359e66", "derive": "346249306132497649f765a746576fec3c8640d5648f6c821e01c94d6635a0ed"}, + {"seed": 28, "len": 33, "xof": 64, "key": "dfdeb3bedd683ea5e7be357f352108909e305927738447cd1e3b12868f09c861", "ctx": "lambda-vm oracle review ctx 33/64", "hash": "9d2b24db2bc6fe675948d163f6d44d6575581aa479afe1c1e831044c1af8872a48a68959d7b6c82f8b8593a69af6270c73732fa96b5c53a83a5f3e73a0c7d239", "keyed": "350e2f86f87fae4ec59a64492eccd9da82d59a3e995b4aa5adcbbc66c1569fdde120c789a3a7a52f41fde3707ba3a06cb4a1d35af80ed299e9b3fccf538ea0a1", "derive": "568cbc5904963dafb7b00e79915b0b684b153b3a4a53d11067a7f01ef6b84676f75ca7844f50dd15ce9c60cac2e7652828df27ba9a671545b53ce885548f41e5"}, + {"seed": 29, "len": 33, "xof": 131, "key": "73092fc14a279f4086511f0fb033b375c241f8786cac69a76b9e7741e23ab604", "ctx": "lambda-vm oracle review ctx 33/131", "hash": "bb438da5ee6ee50a61e2c984008a4b816e327c95f6a47ac8c11d7ee537c0706eb77612e0f2dad149286b098f892043a62cd91c9fb3856556d7334e1a5de3ea36fd3df32cde9e7d68e3efcf38b4a0acecce095c698d464765526d39146e66a6f2382f72e0b2db791f43844de7975cd448d1183a9aa6c5346941a5059349e372dd7ea593", "keyed": "1adb706a90fcced156afccbacda27db9e57ff84b19b4f73cc72cdcfbe9221b01832e8ef79ef9829f9fd922eeb4584ae68d23d61d19516048748e3414a9d0c495fdf513523a881fad9f36703fc9d6eacd0e36aa3d79deaa64bc6ec476fc5364b70ccb188b1400f77ae4038cfe490cae20f58486f33a6c0e0adec5be45f037631ea24c08", "derive": "08bd3f3f80c8cc8d61c67f361948a703b3c648b00df9543c02e1d894a8006707a204e9605106f2ec362e4e5a230ae89267ff2708aa45d04be8f4e5be36b539c3138bc847f827671d46aa4321c5dfbeeab4ec376687bc91f9df331dae0a187ed31a88b733eb2176438d773a111158ae75561f32beda6cf7cd923dccd14350addc44f4f4"}, + {"seed": 30, "len": 33, "xof": 200, "key": "2a2c4ce3bb5f2815a142a9155a0092a92a965f8cb20c2100498799b76422b677", "ctx": "lambda-vm oracle review ctx 33/200", "hash": "dac08212ee797b2e713b25a596642be078022fbcd06859a4ceb0990e105f971581078d79719fce9ab8baecf7d44f7742c5e7faeacc67795455549120cf86d2599af82d19d76b4df7c7a9332eef0589a3ea71ff56f7d762adb57617854ed8340b59e5ce7716f61f7e16fef7db370a1ca355df79948ab0635bc33c8df93d72706344cae8a9d94cc17b35b869aa6bc28257e871f336ca931f68b5ca2594af9ad1b4518e3028422660db19d48a810cc0c837f1785e0d94091c847799c732a9b5a9effb41823c6f32030c", "keyed": "888a4c62b8c06353dbd9889296c692675f44f803a5465d1395109e0f8a07f2b2190490613894aa47ef30b3f545daaad007df969935d3bec5643320f3660ad959160382b47c1559f4cc5351ea29fcbbad62320644a1546343f7c1717418207b239abd8834f453ffa07b12c743f4d697dc8c8ef10eef74967c50a6d425afd387b1f13cc97f53774810547bfc3f8377a76a75fa7df2267e19d62dde7e5b84a6f64a6df693da3db93f2c20debb4763341e6e70a79cd290cfc5393336d4c18ae70f3cc11263eed615d07f", "derive": "4665c81a65f084fc050577c83edcc924438c5ae94efcf08833de53feb697ddbe43a6114cb825a7962455ac7834ea85add26679b75e80abe73b260e3db1f604cc570f918bf97eb9106d029033d92bcfd8bb800d160b9aeed608c94a098837cd09a8f04cb5c9a9820bb100740ae5f41023831a0fdfc40c7676b3112cb8865911211efb9f39c3f8abd15d6c255b694c94e151f922750acd1957b5ed83de0b8ddd28f685a14855174ceaca79094fbe6f76be6c61c9f7f28d582bee692c246f325cd16b7ec854f8583e79"}, + {"seed": 31, "len": 63, "xof": 16, "key": "bf567d39b3e6ef07817fe823e62a01cd76885b59b1ca71618f1145521868d4f0", "ctx": "lambda-vm oracle review ctx 63/16", "hash": "4262e019e820ef30e1e25fdad359c3ea", "keyed": "9f6b2487c3fd74c2a9d9be0e2581fe2d", "derive": "caa2126e0420650fee91fffaf7ede68e"}, + {"seed": 32, "len": 63, "xof": 32, "key": "fc4dd8ec37ce39f879d8e00a0c73114a45e91730dc70d90d18e5e654c0538431", "ctx": "lambda-vm oracle review ctx 63/32", "hash": "aaa39d28da9cb6b404bcb7d32a2920ba27738aef1c3483a6cae69684bff16889", "keyed": "7f999b8c7266cd4d41d379e431090084c2cb66cfc23b94b90de8aec7e08d04d0", "derive": "1d27ac79f3f877ffac23c9398d7c82514b41939ac6ed4b5eeb04c355a0a03802"}, + {"seed": 33, "len": 63, "xof": 64, "key": "228af733f9098e51b260f6f3273f85c2c90dfb84ef8e22d8a34b97c534a28a21", "ctx": "lambda-vm oracle review ctx 63/64", "hash": "d05537d45f4d3f83c2a99116e2cf7be2360a0b25ada3f5f5df23ea147ee2dbeb3839bdcdcfd838a0a8515e0648af60d51c422b7acbce9a9e6b88b1783b307fc0", "keyed": "7c5849505cf5500ff148af37164943ba45fd7d471f23e7b81d8cfdbb75ad5e560fe1e35b063ede7d7ee23ab2b82b3ac68495e05623b3ccb8b3497a5bd7cd11f3", "derive": "33d52e8cfa97f0fa1178a5517f8bfdf44b77ba41488613124d3061e601a1f09d39bab70e5a1fd7bf50d753f58d52773ec61a1969e103265b4a900a4fe9c8aa4e"}, + {"seed": 34, "len": 63, "xof": 131, "key": "2576e5cef6878d6ad1d871bd733ff3b19eb40c1ba2382108f942ca47a157dbf0", "ctx": "lambda-vm oracle review ctx 63/131", "hash": "c9e1b5beebbc4b4555f390e7671456ea4e99bd0c609cbc4879cd7993bbba429a1796e0c86ec72ab580ec6cb97624d1c4fbae8188d7e712b666e41f73258a6bff44ab4077ec5caca38a7433be48c486e99c165087a546594e84a04518d46d572f50fc452e62c5123889bd4082085cfa4dece385df7263294220fe2e284046529658fed2", "keyed": "61dc08eddf340491716507f5df1eb74b0864d8a736357041a057a556b5539a69ce82f9b91bace73c49b01d841edc68f1ab8dab66b8d0e87ab8366fba501f7a5c4ae280f7db9f459187b18cec8a753fb500de2b7f6e4a35f7475ee9b71d0f570407106db39e6831e609f03957fc1fcebc8799536498fbd1d713b2671474004cc5474594", "derive": "da1263ffa944e8b880d03b5e8e34ffc10a310abf855c028bd483542e51ef3fb27f50c9bc25a276bc993255b93c1132ba8fe8870e8a8326fab129ced339aa145930e1ff881082ca6b9abcf2dd11849b4fcc17e3d8e871b331f662b088f2d32682f96ee983e913867b78eadb514a5d57eb9a60574b6d5b5c4eb9ec42fee23de04b77b8f2"}, + {"seed": 35, "len": 63, "xof": 200, "key": "4bb395e425b26cc9923fcb8a4b7d3b4780c76f48520d1037b950aa020de21884", "ctx": "lambda-vm oracle review ctx 63/200", "hash": "7f7d40e208d339f811314b5eb5d1bac48af4124805f7a02c78240c0f967e67c29a41d9a649308da8c46487bea1bf6eb8e0f3a38eb7175e68cbd731bace4048adc87502e2708c7579a5d070c5c0c6fdf57164a8a9a8b7b1329882d0bc0208b5bda0717706e61f677ebf648fc890ecd8adc02df65793c0b4500c96695c832d473d97962410b3c4d4e9e4431af5990f22082d6aa4ec841067b90222972e63d8b387ac52e7bc31d5d7861cf4bde69a46ccc376550144c92fe0dc1b02d3355930a017bd1ad14b0690f0cc", "keyed": "1048cb6fbe2f95bd6d2dc387c01d9efb207f63a30adc9b34567e9b7c364380cdd90fddf1cfba6fb406bf7dfd24c4d84c17c562f20459a739218c839e31593d69b4676fba089efd4367b047c10b2f4543a24fd66a9913f4ef84cad9a299382ad63226d507a2f5fd97a3e54792d71bdb2b6ab407f6ccb2675b97e5f56773f81fd776014c88cf04d0a3d0adf2e715a8f6e94876da4dde49189cdad6bdd4a460df60869a03b5a2c27a2375b022293e7f847893874d6d1bc1d037fb812035a4394bc8a7dcd2fd67ee4afb", "derive": "ebb1db90953880b49ed3c1cc8af140b21342c04fef76b6ab6580f7ba7eaaf332d49f38d1ee789b89a5a179e977beb397ee57fc1735a98e5ed7dff73ade7df059c9f3212d358c1132a6db7f12348758c7c1190348f703193590a6495a313c9cd746cdf4a2b41228e4ef0e5e8562e7e31825a9565293a545bda5d536fb5060bef37025fa3f17998c237a01a22c430b6344358b3caf4e47d06c0c382be109b97ab283459bc6fc06c71309dc38f7bff007b78efa679eaf7d34ae4227f2348f408f6205409b2c3942fedb"}, + {"seed": 36, "len": 64, "xof": 16, "key": "dcd8315fa4d93ab7401b877d6faf3d91216ef5be067af7f88203136a6c930cd6", "ctx": "lambda-vm oracle review ctx 64/16", "hash": "d157ddbefa378438f4da2f56ba3999d7", "keyed": "7d9689c610ea2ee1adf08f5fbafda494", "derive": "5aab60d350237580fd2e72c7fc88e25a"}, + {"seed": 37, "len": 64, "xof": 32, "key": "70712f68d2f62b0ca8b89e939e02ea75ce4906752926f53e13e5123e565c2ab8", "ctx": "lambda-vm oracle review ctx 64/32", "hash": "2f9e3ee7adf6cdfe9c2eb4c5258a7a6407a7ca7c6097c31e9016b7a2d3041a6e", "keyed": "811d9602e043f9300dd08eef1b7d80d2636dce0beb1e3d3021375830073e779b", "derive": "779db5d851ab425e380f54d3e3b7dbd35e86163d5791b7a7e6596846d54fb981"}, + {"seed": 38, "len": 64, "xof": 64, "key": "0501a6428cd4abddb3ba9d7c7c7b7233b7de463f9b2bc76271f69c2587bae2f3", "ctx": "lambda-vm oracle review ctx 64/64", "hash": "927bc3149626165538fd524c3bbfc3b9dfc2569807604b16625f7f1a6284d1ef4c8f416a9ab9d7ba0a345a302a823d5b5cb1c8c6a5773d3d3ebfb894013b6800", "keyed": "dde8934e7634a0d90b5ee4875c1cdb9efbed89f6772920f6b79e355385ee69cd431d8038ecbc511073404d93af5e5a7e5458dc7c661f229d509bf012c4c04696", "derive": "7e5cbf68decda9fadec8f78802089a0b5174adff9e5dd810e048361ef1ff2584914c0e5c60d13e25751fde8a581879b4400b857cb9953e9210784cfea0919a63"}, + {"seed": 39, "len": 64, "xof": 131, "key": "999aef198891c1934233fe032a2b70c199be1eaf910d9a67882bab195bb665ce", "ctx": "lambda-vm oracle review ctx 64/131", "hash": "896f9dcadbeea0c1e49fe27f7d1174641cd6e5a9c281c0b953a4eda56e89b0fc4ba78af86bc387aaf7d8de8895fb03e759242a139971db04e2fd40b63ddcd4cffd009e5eaf22a8e39fb3c8d1bc12f05983f4a9a033e7b67fe505b89ab36af9b5ac95874685e6b6b27d78423187d00119aa2cdc3a273d1e1c08067be0452831de1e9608", "keyed": "0e51ec8678880a2633455762a587de426a4872976fd22d77d6e5911e97acd1a880178c3c23cf9d0bd160e0d892dba818d287da315646730c7da310809c43061c38e87f561025c098e716edff0196671d113fd61f7e85b5b6214be0221549619effe0aee19c32ed46c4a2fcb2a8642278ab8d06caa80ab65206cd974b3a2c31ba191859", "derive": "b2c4be9febf61f6cee8db15af3c3db73029ff205dbf16af4254b0a069c5a948ca7374ec85f27821bf9f6d144d1270c5d1e080c811844fb24c702d9f05e9804ff83d04e1decacd935d307cb5214cb1d2c26a177f48c6ae6f651ee87fa73977b70ae50c8035df33d208af5c2a0941c68a1c5461f5b8c39abee9b37a5ec8d15b61017e458"}, + {"seed": 40, "len": 64, "xof": 200, "key": "0fc85f0e2a4062e32fa0ff98397bb7d36232ca22dd0577918026b85492ca3a6f", "ctx": "lambda-vm oracle review ctx 64/200", "hash": "602a68f3084d758d683e9bb1e87f90cf0eb3f5fa9b1c72c67ae7ea73844265307fdd7bd060407314419dfeb50831db7271624dcd8a306666a73e2eb7dfc7e0d802dc0521fa05eb69203d791e90b1e4bea32887225cb5980e2631488f10f0d18a98f09a62030d4dee3f03f58a0f0151d3c533e731df86e4df2bafe9d395df1f7e43e1ecb51c6baf1115452d04c11cadd5d5fd467fdaba98bc04537ad265e45c8a52fea8831c8ebfdf37053576584270f4ceb12d4352e3a4ca386d0c94fae692f94bc264d956fdf6a0", "keyed": "ff0f40bef8b7b7229ae920abfca4a8c95648070075d8c960f1add315ecb9528df208bd76e4755128e09b8db80d5f3264ebfeda150910a1bb9597105681632a6eddf071300520c5260a2be70ea6840e2e9678ff236aab3548e151cb3d50cfe224d9d679c0c63ade61af9128d3ad2832e4370820d896486b3aa7a45edffb2a2a20b9f6d0390a3d0021c81be5a078261d05d3ff2c4b1fda0bfb20db84d70e3e1b3890861415a9f0be072e923e36a8f58e06003746e51cf12bc001ae7d7fd7afd24819c7aa4e7101fe28", "derive": "05dc3dedf0ff11444fc56c14533af0fea2487ec806b6fb375447d898f91d31df614676bac84d24a592f9e720be0f75b878894dd31d2cf91c55e98c029a4b966e3d8bd014d503040fefa66d032d47c4f6f0f49dc8ec3b417c75e2365bb1de95cc3ea68a933a15330ab3b9854d93715cf3761cd67769349c5e2e26f68510e01d7bd5145940aa2fe0fb6bdad0c4a0f3b2644387acaa6a09df2da5658686adb1530620d9fb3fa0f8cfa117993737d84c445c0aad5c3405efa569880c34f5f52a03c4edcba84e1307c607"}, + {"seed": 41, "len": 65, "xof": 16, "key": "3405ed72ee6e1e23b3056a704653ef50f87fe7b96d04fdb34006c9e4cfae0999", "ctx": "lambda-vm oracle review ctx 65/16", "hash": "dfb16d551fbe1c4aa129d94330963e3e", "keyed": "fab08e278a8ce8560dda049029890668", "derive": "b602c3e3951abc54f9cb839e0614a03b"}, + {"seed": 42, "len": 65, "xof": 32, "key": "5af3a92e08b2d165afa1ffaf4b4a718184038d58cc4f9ae4d82c26efd3f990c6", "ctx": "lambda-vm oracle review ctx 65/32", "hash": "4c7dfbf6b14dfd1b1b6cfb4d7cc5fddffffaed6a8b0d22fac9d1c2a23659d356", "keyed": "19edef3cb85a9dd2aa998b98d32f1894962d92f737a5d90b07ec6cf56c57631d", "derive": "f2900904342a7229e5a5291301b60d6e188d591338ce5a69393f0ef72ef608f8"}, + {"seed": 43, "len": 65, "xof": 64, "key": "8030ebe642164919b2ff6dd0813434a78d960adadd27bf31473939e8b852211b", "ctx": "lambda-vm oracle review ctx 65/64", "hash": "e1050d263b6e42425a3943383bb6decbc90f6d7efac5353c2d71bc7db0555783781da185db68be42db253d7db0e473adc4751ced78b31aade695fb6173743825", "keyed": "701bc44c78df866dac167e79d952882c02ed7360755cd86ba79900f49630eba449fea7cff119f08a77086143178fa25b866effd36b4ff7928bd9baa241b04bad", "derive": "f52acc57658d78b7b9b320788eb71fcc3af457c04e12c94068d7805529815ee65af87693a00b873b332f5e9b4084aedbd678245493d2a5d30a9361abfcd96ae3"}, + {"seed": 44, "len": 65, "xof": 131, "key": "ee528981c9463bb1736ed4617b4d514eb78d9e7589f2169a2e9c63de8ec916c0", "ctx": "lambda-vm oracle review ctx 65/131", "hash": "36077359619cc4e2676b8fa3026eb9c6794bdbc76d73209a22276f7ef6724faa9ca61c43e9851dc56752d630469146dc84791fbd82c8213854c08cc4fde6148b19486a004872a9b54f9677ef41f3efcf112d3dbf217e8684b6fb48e002b78292f01bae8e0bdd10411d5953dc17c9a2830d1570a9158e15f76d1b67e371078bd1adcf79", "keyed": "b76a2dde6eebd30fc4844e18e704a7d3a128ffa2b996aa89b8f8d8c57400c67c4bfdd7591266d43d359b9e89ba680b6ab6145bacd51d72c2869982fb6bf7c526b319c5b123dc6233f5664f54922ebddfd1d8d25841c3bf7a12ac4a044292ba069c318fa73d29ec98fb86a9e25b0f369a3b79c3742e12e4fff2f8672d38c8b37c145df2", "derive": "77c702e02e4ff332882554f0cd9310ee7be65596c76e1c77e50b7df84065547f71efce4cea00d8bed8bda6ba8d1740ded9ccc42fe1685c3c89f6624f0800295659d0fc690f4bfb0da9945790b9ca93a44bcd48286400ea9dfa3aa38ba0dc1dee2977a932056cd0ab02f96856a44919079c03359201fdccee8d31881923495e0f3fa163"}, + {"seed": 45, "len": 65, "xof": 200, "key": "83ebf4a74303c8137f44412c714708d00e639b7094a7810fedd9ab0c28165013", "ctx": "lambda-vm oracle review ctx 65/200", "hash": "a5e8ad40fb449a0ca7f50cdf35b8b5f192d84c646c48e1f3db70a8ad6f9de5743b26582ef3095962c2e70cde4205085562fa2003bbd4d6ff05d6e549bdf140cee596c179f3b4c83f06e1900fddac36630d16a89000010a2dffd35f70019fe3d35084e0171731907acab1ada9de117ac51a6bce94131931f4c1d6d2a16fadc000df322010c869c9e957f3d23798a08854abf3214792033766e0e6c01fe14d0061a397e264e4ba36e08e7262db6d20114816d1bd5c952e60b233bb83628da2161321d09ab7794445c0", "keyed": "ac1ccae4705f7f9247e0c6e92fbbaadb4c07dfd817b81a0c1ae6e6c3b8cf10a7281ff442a2991121b383029ad9fb0896965a77a4f8c0ba025eae95ed1acf98fcd59901212fae11e458be939b28ad7ab45eb0eb74e2c54b8fec25ed0c8dd2e4c5f785d98b07604e03e87f4936459f078d05c9ad9c01e1be625e61a982b6f01ff3398d47972624ec97cda2e421f73ecbc9f6c95feda44fd303c98065c1b848b1c623fcf9b2ddf7a94198894a9733731b5f9b33f93a5a502b39cb95d7a2c98eaf9de101f8b8d1e01706", "derive": "18b00d90ca81bd1c666c78c35bd781310aac74c7d982d6b30569bdfdfe157d2f2f41163cea7067b9d20572c946ac04344f8f680ce8b57f5c2f16e6a5e6b9579315283b0e661dbd1c873aa1061fc92c9e6c173540fad0a591d6886ef2953c9a6b5d2aef3eab4955afcc18e287b3adb4127b55febc507fa290746c8d5ccea4bec4ad278b3c06feea13725fb6e527d7e5956b65bb5743744117ae79d9a604055be0425cbd0007b127279a54aa09f5de423a72ef5b3d1421f5ffaeefb00a9adbdc338478b9d80086a385"}, + {"seed": 46, "len": 127, "xof": 16, "key": "3a7db0a1fefa0fe7771a95a8b9d8f21e7f7874fefd179b050e46904740340539", "ctx": "lambda-vm oracle review ctx 127/16", "hash": "4778a894166f5af5a67a7efde5af3adf", "keyed": "2dc7c28af3cfcee3cf578e4695d9b138", "derive": "f7912e7c9bd409ab95c01b5b7a068585"}, + {"seed": 47, "len": 127, "xof": 32, "key": "ce178a1a3b9de3ce2d077eef0e0e99be95106dfc3c0c7c89d99186078a9fd209", "ctx": "lambda-vm oracle review ctx 127/32", "hash": "cc328f5912ecc945f8cc29a43e6091d914392a54af859f09607d836b79a3e745", "keyed": "36a924463ef891bd94690c46fa412e395bf7bf812a9463645dd6135f08b116e8", "derive": "2b0da95bcadadbc0fb612055ed5370b016f354b620b1a5b7d108554bf1986cf4"}, + {"seed": 48, "len": 127, "xof": 64, "key": "7b57a2d7709314e0891179c6df8ed261124481c5a5619e735e48bf762831ab09", "ctx": "lambda-vm oracle review ctx 127/64", "hash": "acaab09bce3654c8398ebc7d7253b4b103d22133b8e0c880612b59ebc0575a1a786286c8b9278936372b029a79c963e10046142c1dbce65ec9048260cbcf6c5f", "keyed": "a095b8ac8eb13e496f2afa818bdd18a43bdda3959c2f2dd45f45322b6d0891a7c6ef9203ad21ebfb9da8463f13dd3f92f7bc461bfffe8fd9c12e03859e5cac1a", "derive": "a4712351b39281e204f97908eccc9326059fd4498f833ee9531bd083c567b1506dcc1170eed9ac0a3a88fc6fd4d8531bb756d0b2925ac515a68ef8a46da24bae"}, + {"seed": 49, "len": 127, "xof": 131, "key": "a103179229a58b5516244c74202acf3458ec9c505c675fcec68f583eb6d4610c", "ctx": "lambda-vm oracle review ctx 127/131", "hash": "997bab908b56bdf7ce69a0f1c6a4eeeecdc2d5516fd7fc18cf29a959fdc0e85000b8074e90d17c3fe63213476aa17a193b6f42e1a76db2ee05d5b511ca44d59fa84242d12aa698a21b63a7b96c899cbaec11c95214c0d55dfe5465c9f977c02ace60856fdcbbb35a7c591da0a1cdf5a9cc044c0d87f778acdb258fb21ca6617b51c29f", "keyed": "239e5db4ca6fe92c80f9680341715cbeafd6733cbf111248088215b72223553f47aa63c55e7a15bb8a9802984f53fbf43033e3c23b334fa16a06b59c18551ccca47b34c4f6fbf1c820942efd2d55cac5c3f9df826ebac493d70cb7dcbef7d4d2277a595461d7a8046c544ae779fc327efc6f3e56bc16ce190efbde419cc5b03fbe4d8f", "derive": "b86ba40d7ae3eccbe5a6d9e4533e65326a04355d58d803e97935b524b937358c9ba594a6053e38d043bc2e15396a77e4c1285000d8861244bafb12f66e740b9477c80f5d980da2da1cb40ee06fa3e2f639c8e7e37ce6e217161efaea2196d1d3d8d22f11716efa004cfa19595d44f78c084352c0b99062584a8d9e7eb29292f7cd7ebb"}, + {"seed": 50, "len": 127, "xof": 200, "key": "a480d2a7c82d1bcda32057f4f418bb56e9a6384fea983e669b9b9e75f065e6b8", "ctx": "lambda-vm oracle review ctx 127/200", "hash": "9cb06e7f440251995a8b91c1f92e5ff1047860df725c557372f26d4b14f80c4a7b5f294659a344c67b04affa0bf0a0872f64c7dfbf2c8336c88b09dbfab82250c389e98dc0c0a92ba3c850fdd7d14f2e0c1d3b1f8043d82aae6216a528dedafa75bbd034bf977dec016ca5b943b7fdb063fe1a4daaf7441b66ccd1180b87f3382e262ae1ea5e864424a123ff1c4886f98c8c219a26d2857a5724dedd58d483d3a7b11a436f15ff94e7d179516e6b900e0fc1978f53048e27b7d28ca5ccb6a0221abfa605fc3b1616", "keyed": "d7c39ed0632dc56e37158d34a01fd4983b4e3a80788e96887c7e4d98e27962689b3ed261b26b198b64919703ce60a32a620a67887fd956a03c9b07cb3d0a0bd1dcd42252b3ec4589573e70c5f7b28c78c9ba340b7984865a276ab331da59d0a7a5400593c4f1f55a9b3e6a0b3e7c49459254976ad5eb514171662d6eb7700a8332bc233252c9bb11f747e741df39ab0d3aba1202446e2bfda08f8ee69b77fc4d21a5ebe00096381bc15bb9508fc775e826d1d8fbdae063bb9780993e25f65c60c2dfb28bcf9c6f41", "derive": "e1124d9e220190870e3f796d4658bd050485df3008274f1449cd0d63b2ad056810a1f5b884141401aed964d6430d3ffd9d204d7e7b5fb0bf9c523cfccb1be6fe0910b9eadc21ca3dc8be146a96de93906937efef45a1b18c40f5a69524ecf651952f117fe8df66ce769e276736208172699b23d4db026d199c587ebd3b5cacd186ba696e968ab96ae32f4f5dbe52d15ecfd5fc6eb643b86058c1e1efd5df0576d1078126efd431f4e24474a762a013fae60bf874a1e75c29b7055a405b0c6dce05651fc7107c4e6f"}, + {"seed": 51, "len": 128, "xof": 16, "key": "ca2c9331b09bd8318cb472ef0ff2385f367fcf277359132fe18225b14e18293d", "ctx": "lambda-vm oracle review ctx 128/16", "hash": "7260b268b8f6338baff4c78c44faf52c", "keyed": "e5f4f67b8657119ba6aea3e641564caa", "derive": "9a9c46b5b7dd0b724aa8b0457b7ebc56"}, + {"seed": 52, "len": 128, "xof": 32, "key": "16dfa94a87ffe443e3dec0dc13f3bd6d19796c33de8e376ba772f551b9e59d8d", "ctx": "lambda-vm oracle review ctx 128/32", "hash": "4a63e95e25c4670d94ec1a1e01459041794e05b2e5a9bb1a7818935564290013", "keyed": "5973445a97d555a406ea562b257248f44a478e11d3dfca0b59c83c8123e60557", "derive": "6a43acd3d3798a352d137db9e0afdec0589c6d00b5b11d4dad5baaa4473f89f3"}, + {"seed": 53, "len": 128, "xof": 64, "key": "aa0ab7c74612cfe86fd6b0258a8253a5b40fb2c37e3280d3c99c9d641ad8046d", "ctx": "lambda-vm oracle review ctx 128/64", "hash": "94745003839a79dc8c227e95dfa24e6ed4de58c15b03360c8c16b4c3476c7a191a9234fa8180c5dc599f2664a94eb7d38253cbd97114942301d3c24ad0375f74", "keyed": "c01eab4bb9511b877e0a9b1e20c47d99a9ad9517297eaf140f5503a5becb4773b121527af1060e320e51ce5c60330f3f59096b08cf7794d828c3e12605d53f0f", "derive": "961479805b01dfe530e97214ea5ea55ff58b2b1a0b653ed7087e2ed83ab50ee39548af3dd3f50de83b067d2b97546bc41d956244e9e58a4ede6266dfad32950a"}, + {"seed": 54, "len": 128, "xof": 131, "key": "3f08fb1a9b5ee03886bce106e723704cf0c3c1258e196572e9bf0fa2d66f70ea", "ctx": "lambda-vm oracle review ctx 128/131", "hash": "1c018763f8d48fc81bbd5ab76e8e20b04159f50e19fda1406aedfcb712f9e018f9ddb4d9bffbbb29b38ef100700c68c8fd97cc14b510b77c746090620e4e1198db41b55e25ff6a08bba111c0287c61b5be00cac4803954d604ea293d16b983f53a3287ed43ed1f02b64debf46941a8273a3762cb81d54bca602dbd02dd525cbbef0ed4", "keyed": "3b46b8a4e038f114a6e4a0d5721cd4ae79cbf2dc28227ac353fa7f24f6c0248fc38f39da6fb574fb82e138c47f24824860136fe20023e754c9e603e20f2601b1ed7c29d52a9e555c681b663927e9f168fc423d077188224d49167f7c15631a317af5af52652ca91133285810e646a7efedde51452bcdbd889a296c821c77cfad74d625", "derive": "41618dc6fe81da9ba6d4a79f10381a6ac4a39959b1741082b18e9314fccf1e82f6a8ca667aeffb4bb6430fdab35f3e3df1ba578fee5debf55da9a1d347304feffe5139864ba1bf6a427adbecedc3f80b197f0d67ed92c2a8e986d52dbc0fc01aa7dc92f7e666e07e0cbb7ddcb45a7d8843d1ee417d97e3a409bce4304f36650050f572"}, + {"seed": 55, "len": 128, "xof": 200, "key": "d3329a66c41dfbc16d0b8e80fb27d046b6514604d87510ee726c05808310379f", "ctx": "lambda-vm oracle review ctx 128/200", "hash": "58bda839d3594b2568a91d58139ab02d39a296ffb3903c766ea08efb4c94d3f360c990b52efa7d0b3e5ea44fdc0c31563aeca3142f4cfb711f24739b2dd20383474d2c7999c39954ea4bd37860ab1903a35b42e826551e619111bae4d0a9b682676fe2ef9c12d1f55706ae1b070a59485045ade6ec1e7c335b1b4b98b5d7b73fb3f17c659996cfdc79054b02be7361b088bb2a27044041021949e37a147bb285142a218a1c6bae7c7a71a586822f1665be830c46e055ce443b3ded25793b8d151a1e0b0c3c031ae0", "keyed": "d3f6bdd9f8574460d01f25f2cc463b007cf28812493d05531ed305963789338306b85cb83986e0923fe12c1a30f79326255ba8406ded0c47cd6116f18e5d09f299fd7579f69223f91a546b535481a292948c832354dd4a823c2b458b5158f1c42a6fcfacc8ff5e35d047dd0a45daab8ea491e8e9622ce71ee4314213c0487c14dd7ef310f0461f67556243c5505dc7f73a5ae676b2ef017502048f2fb9ca4f84240bdccd955b6d2c9b50eb12dd5cac000d08b33d1874b910bd6bc808e25b75168476bb04ea76938d", "derive": "6450a7dec0869a1e3c8acfe39112f70f3d0b3e8c296f0c3096235b2917624fc98827f8cedff426ff6e189bd360d64d77fee2d293dc5f62b8b1a0ea90b73bfb40ffc5c139796e969bb589ac8afbfe8b8e501142b8e2d966d70e86c87de016ecf7815430225cfde36a706090c3e71439836ea38a481c6bca101ccf496de13b2ae4919b7d9d426c9d41560c598b624e45b366020a850c4e135f1c44e99d978f4d99d02a076c51080e6e8041552a9b9ea8ef6df548613e8d1115a40643c658a444698a75cb7a334481d6"}, + {"seed": 56, "len": 129, "xof": 16, "key": "8ed29fb36819743a8aafa7633b901089aed972c78f4e659dfa303fe3e7d5f763", "ctx": "lambda-vm oracle review ctx 129/16", "hash": "53933218cd3a7dc226dfda7657611642", "keyed": "f9806797d6ff511f653fc782afc27c5f", "derive": "bff8e1942bdc60787105dee8ecb3c163"}, + {"seed": 57, "len": 129, "xof": 32, "key": "b37d838c35afc413d4f4a0834775240e29b5cee2b4a4fd4cf076fafb3f075341", "ctx": "lambda-vm oracle review ctx 129/32", "hash": "788921dd78ce7a8af5f477677d8242ab2c1fec000dbea6e20356105594ef6977", "keyed": "6719b59a5e51d0a42ae683629e6d8b67c5c49a11cd69a78ddca2fba1af7767fe", "derive": "720c8d0efea80152a623dd2b67c8e707fbb93f2dfbdc6b2d39ddb1d649b61ec5"}, + {"seed": 58, "len": 129, "xof": 64, "key": "d9fd0cc1961360d4031cd4d5f1ec05cccb77565ef4b82eb4ae4d6f9a7883a731", "ctx": "lambda-vm oracle review ctx 129/64", "hash": "661f362fe1a4536f81abbb67e8c8505e9ecb0722aaa843a51f32613db21e4c16f165d3d237eecb99260e3ff88f8b43f3cacd0e323a9d0938e4ccbafb3212c4ff", "keyed": "b2b0f1978f87ad14a9d583d4cc5512192e15a3e9f425213a44895cd9ab3541023609832d8bd47a53d23d6a6ee091a860c8e5d3e8171c320c7d8374aa950b5a4d", "derive": "5dfa9b9f0ac8703982bd9f1dd20910601acb82b9d5a4cdcce3971b4fad21fcba9b5fe2ea7247dbc4f0ffb21f46daa8256ca0f04eed428f05f544ccd34b1754b6"}, + {"seed": 59, "len": 129, "xof": 131, "key": "ffa85eed76fc44128bcad9bcd7ae3713c2dc1206f3c61e0ce2774834da5e0617", "ctx": "lambda-vm oracle review ctx 129/131", "hash": "5a7a52e7bf5e79814bd861e2cb80e3af706036b544f8ed5e84a57b32e0acb6b641d009f9fa8d095ffdb0f00374c743c1c34edf3709f88cd33ced94e99887e3b1c709f0b835eb79c8726a36e1f032286d70b474843b87f6be8fb55584ef12b25adf776ecd544eef2794e72b77821ce05ba955d39be3ce18e791506e904051747883e6bd", "keyed": "98c57f6aa22ef67c09b40eb6c9f25133e3f439077d6f44669c5fc1562276affeb135ac5a030b438549803e05f07c384b1088cedd3849ae5dabcf2fce16fb7bac3065ba7b03de9b3c50c03508a29402463aa0876cff9604a71b8537856ddd8b49b0375e3e5582cd149293680909f40dafa59e7efa9ef0245ab3e810c1702e5edae2c261", "derive": "261ded465c98c65aa2623b43d3661f1dce5c48276fe0288ddfebd50967877716f60ec020080e6364f3c9577f2c0fc806b960eeb0ee61d96aa6e3340246e9a089f9bcc13d689454eb14a6994f5e6aeb2b5efd1d7b478557faf83a6b6da686b8a3f0aaae6639a66d948c3f76ca955b9fda44f09500e15cc75d98b999f2f72db52a63278f"}, + {"seed": 60, "len": 129, "xof": 200, "key": "285a6026fc81bdd9abc31cbbdea815f5ea45e1ec71d6617912542068e54634c1", "ctx": "lambda-vm oracle review ctx 129/200", "hash": "1121fbad05a0ed2ba8ff2498d45a9ea7e860384b021aeabdfed3c67f0eef7408ec6a15ec53723904e10c890e7323d1b509b3fdc0ba8fbaba2765eec1ab6db11f0feb360d1c92ee019b83a2f86aeec5554850ab924965ddd94d294cabe56b1db35efdada8d81e0b8832da33a47ec9ce135d96179ce3eeecfd71562187ceb9f5a550dd006d17f71fbd83826e1b1abbabb9719bd4a43ebfbf88572a9576d4e91fbb9da188de5a7cf2f2e2f3aae7ccb3eab767c49f82e127b172fafd76672179638768fb5fb06e946647", "keyed": "398d09d5b5cf7402631bc8eba3a4d4050e07a814376eec7edd52db6cee980fc104dcb8b8efc8a0132c2749f2565c78bc19db1163da064ea20a78c344c2219203fef359e4e5f9a40bbec9d401c14f041be0a034046c424d1c7754c25aa41f7ec749ec7f47e461a6c676b19fe4068f925a7d9792539d4e5acf92d16015ec05e2f4d889eddf55dfa5155098ccb903dc871e54fdbc09e83d546558db1ec6902c0cfda170f2fc2de829195766f65880fb8658d993301976f0e9f1799fbb5d27d38b344ca4d70b91b54b79", "derive": "1172b99ed6904a716737def55363442c70ee1ca85a08b6e62bcd0b1f15418b209babf8516c130a4732480acc4ca3d052c3d1644b71b7c6785d70083b6bbeb1945f4ff6179f9d80e177f44fd956e6ee4895807dfb70e7c8bbb8771bf3447f426c3e418af5bf5142d70090b1de34d2f1367aaf7f28ac844227bcf7633dc9c7f1c220164e9b70b00f1d26abb97c3e4f0746123788739224119e6071d4d7fa49892689c8e2463fb22ed28efc9207f7c64f2e768e172362e3382fa9ebeccab5f3d1a8abd7cb3c837c0a10"}, + {"seed": 61, "len": 512, "xof": 16, "key": "bc84ddc1f4c405a0d70a7a20b0a65b90abd9c1523a575929a31b51356a1406b1", "ctx": "lambda-vm oracle review ctx 512/16", "hash": "128659308e8a7103b89954328ff3dcc5", "keyed": "66ac5531db455adfae33d01f2424b2e3", "derive": "3714887158aff8b39e20460d1b1df7ce"}, + {"seed": 62, "len": 512, "xof": 32, "key": "74856534ef4035d35cf4cbb65aa0513428d5722c34950e422b79253741136300", "ctx": "lambda-vm oracle review ctx 512/32", "hash": "61e3770e80dffcb46dbd31d0f0cfe311cc746d2d051ce2fad4ce235b8249b6aa", "keyed": "9701c520da175568b55b68ebb9b9e38671c9354befc3751fdb9279cc6dde019d", "derive": "f81137c37343d28421733991ceba4f7a7a8445c58daf78a5f941c48935091771"}, + {"seed": 63, "len": 512, "xof": 64, "key": "08af96226b264d763b57413e3bf21605001c95eae5ea05b5c43da130647b664a", "ctx": "lambda-vm oracle review ctx 512/64", "hash": "ab098f1a5b5fa7e09c5681c5091a5c160ff00715c2824a4a2633401521ca46ab86239dbc65f0397dfda02711e457be739b40a4fd03bcf729503e57885a7bb1a1", "keyed": "b48711a18f51ffe38c41060057a47e0174cec29eaf16b10a0a6613cb23fb5b06e347e3ddbba4a2b6dde67587561452b2dd584c133bc5581062fe9f97b0ed98f3", "derive": "80ded338ae4f1d27505253e93dab1949bc51e8a2011ebfb24d99678057b6a18a8b0c7c12eebbc78bc7b343566b8fc7eb4be7705f7e975b35af72dffa911d343a"}, + {"seed": 64, "len": 512, "xof": 131, "key": "96f47b23ef2dd2dc947462660df783ddfbd7c45639e8d2107bdc5646657c5cf4", "ctx": "lambda-vm oracle review ctx 512/131", "hash": "c735b7a9e0afe293904be51a137cff72222202ca676435ae5b41efadcbc12653b85688a678e80d95f9109c10c689718a825311d8e99f5cc1f30bae70420a8e2ad9bb1b8fed710c789577500aed255249ba6ed21abd355de74dfee33d3648bcd439a79514b64416073c7db4b635b107a002ab71c9019de007996d51539ae9881759106f", "keyed": "f9d0d51d41f7a7916272ae75ab5330a4feeb92e927bb53e948ddf5a81d3cba6ebb757ccdb34719226cd4e425fad2f762cedacfdb57f65a63e9169cc5c35c46f02f37820e2d3087b3fd8a64bcd28d67df0ec9ce78a4d2caa02c0c80d43fb005a8b73e7f265c108c72a47baea9532698639dd9e12cc6275dd1b03c7bf687b3bbfd776f1f", "derive": "f5c3a9f44e179eaaeb3380f48f03fcc9a84ab03aa7db263b1580da1c6e3ebaf91daacf3cd305414ea5d90f2d368133acc4b579e6b84cade514e93df6ad20f9f39bfa10a796eda1cb142da2790e445294e3b12f3360d1f65bca819647c07234505d1fcbc63f56649047e4885bf1713f62c9a6b66f1e699d6f2324d0161507997e22c42e"}, + {"seed": 65, "len": 512, "xof": 200, "key": "bb310801144662e300b5f7e356d7d4fede0efe9a98aafcf4ff570ec3968023d8", "ctx": "lambda-vm oracle review ctx 512/200", "hash": "6fb76fa8d36be7d4f31b0bc7c952282740b91f430d12fdf645d2b5bd598da9c2fe8f616b84594763d2421979a7c0c31932952cbf669a5ca8e60ee3f3457c9e21041cedf59045c06e31625ce457205eabf5f7f6a9cddb34a5f79a7092c084915d509b7bc141d29630c9a520277a0bd5044cc3a2035b45464c928bc5e6361c9d2aa1be71be9f88406a2cb8a015df4ae3b0e44fbc709ed80d019598936df42e956251813351d4f7c420914363cab352f7e08e1747cbdfa7dee4c118daf1d6cc7269c34ee7ad2eed2b29", "keyed": "35a85673fd6202f1e550f2ee748e8d76d146134a5a2e7e503603d934fd884cddfb0b1b96046877118ce52cec5459f414606fb538cb75b3373e975566667a7088640cbd4b746faa3705236012ecc94b9a0de54783d679e87587ec9e7d9eeafc515ce532a88a68746fd615189d81f638d3ab2fa1d82a09c058d7a2fff2e91fffbc4a307f2836203f9897b52696448e041bc5628fd6063c43d480c4da2a8dcf16b2ef0cdbd039ebe96a08ed64f3470a026399a13d70990d6b569035521ef789471b904346448defaced", "derive": "06ac2355277dceb26a410851af772954e2e94f4ac8f2f43e55b06b0baa0ae50dfb33744efe8e6ea2aae1fe758c187707bc4975d381e67e173714267ae944d6504f8858e5c9f59a56db538ff002c7d0f305ad253951ace5e363ffc5652410e8ef74aa31cb92bdc3c370ab562dbd0e2094f1d687602ebcc0d3f9a911e40f1f917bb9937dc0d64c6f202a0f051ec1dc9bc9c96cae56fd5618ec9607ec7753a42b7e4e9f03ec73b4ac3264210c2873fd5caf468eb00dd8520e93f7b049dd2f05d99458d7a53126193407"}, + {"seed": 66, "len": 1000, "xof": 16, "key": "befa8b059d09f3a8dbc368577b4d33676a30b41578c029b9fdcf417ffe9a43a2", "ctx": "lambda-vm oracle review ctx 1000/16", "hash": "6f7175181e05840cf5bca9a1d59ddd39", "keyed": "31b3ca8354e46ac283cdfa7de0becca0", "derive": "3e44e34bf6578e1ff975f05cf77e3510"}, + {"seed": 67, "len": 1000, "xof": 32, "key": "e437cdb211ffa77321a331a3bd48759e26cd010375f1d68219457be4fd38b6f0", "ctx": "lambda-vm oracle review ctx 1000/32", "hash": "a7a6ab659e13f7f98a6c3500998ffc3a17416de7c9d5f3b3163cd123838e028e", "keyed": "ecb1d9e27fba7b612c9fb8a4f4715706b3b838a33c357279128fc49b9c454a40", "derive": "3b541d49dbaf94f167bca1d31223ba17009363e27fe1527b771392e0ec2e5f25"}, + {"seed": 68, "len": 1000, "xof": 64, "key": "75c4430c6cdcda06c095d3f03bdbda5b779e718476be00d654c73ac6330d70ea", "ctx": "lambda-vm oracle review ctx 1000/64", "hash": "89fa21877d363271797bdf327d4ba0f311139f593f4ffcaba00d13eabb9837203ae6f4ee806582664a8160a4a065346c6f923a6102e0f9606889040b16b6c36a", "keyed": "5b9f65641c5dc2438c736ea0207ea79f801385078ad64dd33afd2dcc8fbd101e90896b4c672eabefb1be15c3a2908e6d68a6b0d2789614aa75a1e771d3910800", "derive": "ef3972e6aa2c92126b3f8a982bfefb489d86031239d7d6da94c742d533016c602e5b053d60df3549775864d39350ec69c80b38178e0c297184cd014248c45fed"}, + {"seed": 69, "len": 1000, "xof": 131, "key": "0a5daf6694db1d9b7660c1b126fc81066d359c5b5142a13581a2f9a542d40a70", "ctx": "lambda-vm oracle review ctx 1000/131", "hash": "4629528981a3c2c0b64c229c8df8d050ff272caaf4a415032c625bb8e21538174b8d38c7e549836c968ed937cc98298f6deeec12ba4811da87545a4c831a965417181f7b5c59b5b60d1a2ab5c6c4e736949fa5e24c193a61025eb0bfa574fc360e0b237107b3922ea180fb81c5e694dbcbf10c5f541bcb085edcaa56119a3fc5cf3e5a", "keyed": "686683bd2c69e03888ff6a9249f7fd27a793f4573f129bb1aa2afce4d38d2e3118ef6ed035d090d68bc0cace642f168bbf4b1e14d382817cfeef8c51c87ae63c356094a40cbc2d3dd48a321ae5d988ea4c0594390e9277ab97358f76ab01790a841449d04340ec9e2bed929def2490f6d32fc831db678f01466f9692b7b435ce84255c", "derive": "7cedaf8af519c0efd6c7f7b7917b512cf33e3ee555262625041131f660ae4cbdfe96a4a81a3f14542197696cecd174efa1754894b8c30aac8b382b082f8252ea0060d11945ed3e3d6c94912dc4624ae3512551715b4e18d2d74ba41df6c633839f8b33c846561aa1ff08db9631f4ce0bfec8f7ebe94fe02282b72b31e32c373b4b3d59"}, + {"seed": 70, "len": 1000, "xof": 200, "key": "9eca31ee091f8bc6ff2a265069d32a2080cc6aadfae7b2f4473258faf88ea762", "ctx": "lambda-vm oracle review ctx 1000/200", "hash": "4c4c68cb630b73370f497003ed1fb20d23540096ef693cf499afb67ae5285bca71a17877c8c5fce6ed6bc9a78c42cbe77a978f3684ffb6179e619d3d9a1df86145546ade247622455b41af82eba6cd14fe268ed0d8a3e7d50675d796ac8464c42ad20edbee74e584441150b3b77ecb5cff422d1e684823f5b947aafc47c61400696302e36bbc05b367a09d229c153761fbae1604ccbc1e7d9cae9e7f0e938879405c2525459c5b98431c1eb70f8fffe3e909d5b5ea8027eac22fe790202255f1925d3274d7ddec2a", "keyed": "1abe83b666af57dc91ac342cbb0f1ba56490a53609cb4c38c3297f32cc31a919c9ed827bcbaadb32d861539f7920017aafe4e2613fa2c042bbe734da49d296e75bb5d0037f7daf91afc9ed8ec1eee2acf65fe9f12e6d2a43d0b809a6a67d77a28823189e03fd16ea2acfaece5c5b6439322d5d5456248826faef2feb18066953aacadd5391a37c9153fdd776f5dcfec1fdc831869a711a61ad54f677c10467df28362a1ea956a7b8c47fe50779e17700ed3065a9aa0a7b1b8e07c47156afc8d101b67316f199a427", "derive": "31453aadda0b9fc43f8f9e3163c11135dfd69c894e5924e9ae45c17e00466abba8c86a85f3b22a7f6ecdff2c2b8f599dc5dc8ab35b7436e70d922425b598ce2f17f6a10efbc0cbb7a1673b70d8417583a264647e1ccbdc8e219408986b448c7a14aed4e5191587a533eeb4e6fd8b493556ff5e1c28bf8994868674abeec5e1ac8c6eca2e04ada8f24d46db692784bb5e2dd336d5b24e28dca09d80a5c7888a2123cde8183f8ccaeae5225426e8932a1c726ca63e962d2236581c12abda25c6e214b45ea14bf32ea9"}, + {"seed": 71, "len": 1023, "xof": 16, "key": "33630b173362a69dbae2c07065866154f76f244e99f1315197f109eae4b74b56", "ctx": "lambda-vm oracle review ctx 1023/16", "hash": "21d7ad694e7edf37046214b4f872c6d3", "keyed": "9b73988949059085d68ecb8518945058", "derive": "816ad3aaaf4c9ff75502cc055e88f84a"}, + {"seed": 72, "len": 1023, "xof": 32, "key": "32552bfbc19d7b92b714b972065e0a182c3846351fdbf9f8da473a66d07bb089", "ctx": "lambda-vm oracle review ctx 1023/32", "hash": "7d4502d0e2d35b69bb2b6cd2f5ef61e38944736c200ed043d7b2ad52310e7be6", "keyed": "e63ba15cf67e4788f9aa504e42dbe78dfdf51de51aa5fd61a20bfea0c29cabe1", "derive": "6c90bdc9d0743dd9a18bcad1dff79d71c117b8c238fd54ca0af0b74648eefe0a"}, + {"seed": 73, "len": 1023, "xof": 64, "key": "58924bf7db31de95950c6df116b7c3d92d800d4d9194e144d568a96cda1f5404", "ctx": "lambda-vm oracle review ctx 1023/64", "hash": "4efb0d84bcec540b6313a0c6da16c19f6c548d825322430a54b3c1b15d60f17f3006c0199263f3183180a80c47752b2b890269524d0710254576dfa13fbd75f6", "keyed": "58ab643960e5e2fb588ae0192a44ee111dd1ad4ac3edbf46c4b56f69da3732cc08d70a89440a7cc3301f7aadb307a77378b569aadc756f45269eaa7250bd4e01", "derive": "b10359e0c1a9645b79fc3b3c49a4c0f5b8b4a33739e38572521b1908cc3c8b857349e672cc8d7f976b91ccb7bf37ed104cf6de35aa81700cc69123d311651dbd"}, + {"seed": 74, "len": 1023, "xof": 131, "key": "7ea3111b6bd050ee3b0f5804f3b26260e9be092dba7a3cedf86e31c2064c676c", "ctx": "lambda-vm oracle review ctx 1023/131", "hash": "392e9eb9433f4498cd9d9968ab58ba0f7a3a0e07e2187f0c5e35b655ed6e94a1318e6cbcc7fcd27f09a4d48eea14feb22add27d001c4ba2e331b5a8d37264fc034ad4fa8431284ebd1aae6f9b7cdf5e12862a46378b3bb2c94e8271e71979cdd4fe21eb3b582e83db6b50f712012cbcf904e021527d1835e06c99ccaef73a356fd72b1", "keyed": "94fdbd4d70e4266087dc015d7c404db05941db771b69b5958a8d563b2a59736c8df29b4f1462f39a868859446287e2a9f9193f616097f2799512d501078a6f2f16dc396e9a4aa7e42bf84b78d6a83fcbcecefad6cc7aa7e3d08638f4b69f4dc09ac3ff4087b95c90db8aee339dff663710f152c06626244c5ee58c75efcafd9b739f84", "derive": "30a5ba2a68b85a8a76ae8eb44b657782f4b845173d98169b9d79c3447b8e349489cc3141ea763b61d7e0cc825af7cfc9d3d3a78d86437b2e41531902bee1ddef2f5433032e5461840f6e89c691172eb486da91c1536ca59b4403baae9ec6eff03270b5cbc3b11d068b98a4614e3fe5e0354d10983df886cc00c4b98f1fb4eb2c7b9b9a"}, + {"seed": 75, "len": 1023, "xof": 200, "key": "a4dfc16ade4a493f07e112ebbd4a73ab4edfd7d7899761ccebfb8492a295b756", "ctx": "lambda-vm oracle review ctx 1023/200", "hash": "8478f370e33d3592d7991f12964bc4af23d387bb53aac03930f9ae194f4379196a814ec634008448bcde817ab14237fd0e6869649c3fdb74ec94ae5b1c0a434d0d5b3c60d38e53d07da811aebd7195f6c8bafa2a52460eb52ce7d64201c59dff2e764bc643f3579392f90ff9895fc5c573df0633f638b05936fd17fa84cde873e5b95a57e156073c2bc1ca48048f7078bb4520b81d7b1d4acf9558817e024c9a09872b3bf2260909b01ce4a21c594c91d579457749e0b0b801fd6c9d6b9e466a59d621f78d407ed8", "keyed": "17c56f3fe4fdc6d0e3df2cd517df23c1db7e100ec5aa8745b11007608c32c9a58a83573685159f9724b28e13330841964e626f9fcefd62a12314fb2a3f12ddc30d246beb5093926d199762853846e8e2cb050c44f6ca11cb4db8e6a69e0ee7e9c44b8a78c9ba43b6dde1a31ecaaad50d732eb22a7773ecae84098a5f8915562b5bf9ec1713e8e8a9d8d6daffe5e7b2daf3745a76ba1f2ef4f1d459f2e91103b89977e98e5192d10669d6351ed5779a9b44506fd5a707944179c2bb631c1dece54610f6981fe082ee", "derive": "bb14d0c8f0faefb4a6ce5cfacb240ea982419c862da6463a0feb08fec167edb8aabedd5abe2584b5b44508146e47399173927d08d0e05f2a442065c7d68d135d2bb4fa39c3eed3e5289074a695d4b7fadd8eb55f44aeb50195ff2424da1a9b0d34e7dcf73078d7a90414e1dbd603826596763990e46385fea060131d2597736ce4dbec61a6870d9e3b7332c5d9f5874fbede6b078646c6d0fb003a1f0435ad76aacaa47b804032622589fb474b8473e67794c42d99d5360c39ede03da8dcd427307bde1d74bfd73a"}, + {"seed": 76, "len": 1024, "xof": 16, "key": "122524e4b147ba890bcb4b36a1b39dc0588f4c21eb663bd20397e4b0cb2cde27", "ctx": "lambda-vm oracle review ctx 1024/16", "hash": "22c2bc21637d5a3a5c5772a2b41daf34", "keyed": "7914fffe0f698d80bc7ddab03e05c6fa", "derive": "076f9fc6b3a35312c5c79c687e9f1921"}, + {"seed": 77, "len": 1024, "xof": 32, "key": "a7be215c976dfd1555cb25917f28d872d4660e506ddac26586e1d1547ba4a5e0", "ctx": "lambda-vm oracle review ctx 1024/32", "hash": "9d05e80a80d43143baf493decaa5f031fb91f3ce6fd81bcc0270e9473ac94d6e", "keyed": "c3b7affcc71c6efe4fccb3278d0d41acf8085c81170a03470924311c8c6f6167", "derive": "52a053439dde9e4d68eac7ecf2e32e8a3baa17f6ec0e291fd0338718806a9c44"}, + {"seed": 78, "len": 1024, "xof": 64, "key": "5e72720477e1a7523601591e2adf103c8e5bc3c630a43315f36fb8a7cdf62e0f", "ctx": "lambda-vm oracle review ctx 1024/64", "hash": "d0abc07ee8290bc21e5fc11dcd7a2cbdd7ece80a5b916089bd023767d49e0a07e5ee1cc38e851dcf448494397f2374d5a0c370b6a70a2393a6ac06fd7ea6e145", "keyed": "f446907661aef0a2c702dc83f991902ae807f8c009fb16be9b93735456e4d41b0bb01a824d683c45854d11e2bce40bfe5cc82122fd9c465b06110fd966041fd7", "derive": "769c7cb81c0a7a115c8688f45902cb3573aec2266b8784f71443e657e777744a31a031cce9bf5d0252c5380b66dd9519b2c151632aa1a36c1e6790a09d97b094"}, + {"seed": 79, "len": 1024, "xof": 131, "key": "f20bbbcf5654a52a0d073e10a2c95a039f4c386ed0d8aae138eba6eb08cc443c", "ctx": "lambda-vm oracle review ctx 1024/131", "hash": "4e82ae97ec70084c61ae28f8f724ba3ceef17cfb6e2e89c4dd1f075a4d74f6743139e37de17ce3bdc7dfe370d212f46bb580cd659d445ce1a278781a2f83d4a06dac14aed49afa922506fdd0a59267cfc72ead8e7a7c0396428d9e941b6490908773bca2821de661219b6b51ebe33166d3eb293b95db50822e0b6f231f956bfbadd336", "keyed": "6cb89e82c21309efb425ed74278e6b7d8812d1f210b6ba85e7eecf2251a644d9670b8e05e721d60c7c9050ef68e722bc01cb59b1a3f91fe5395655e0805c1421ff3830033c234393af2a9abdae8b06827cfa9e01855e41f53fabe8cc47129cfa5a937530a337e207f86ab70523694c5071ffaf1aae614160b7feb18c271ac2d4c136a5", "derive": "0a9c479d7c63493957c01053e26c54b45ab2a302af25ce1ed7e820e82e4b787f897530604a257932d097a28518722fed68f570f6134c239c55f827141bef08930f41ef0c0e82b9acef19d4de5c661f101a09b8e8d80e47cdc189044b2151dea929234399774e12d44cf0c07390625cfd2cc17aa15eb24db2afad017904f3be4bf803de"}, + {"seed": 80, "len": 1024, "xof": 200, "key": "ff32ba83c1a39f53fb158d0e7b3adde55e6312af13b7143f67a106771f0ef5ec", "ctx": "lambda-vm oracle review ctx 1024/200", "hash": "4c7831baed8e7b961c5196ce58fe3b8954e90e77db26ca67cf7bdb66c4122a7efd33827be7372dbfe6c199374601eeb83c65ae54b3343b54d8fffc0b8e1a5af166e46ae44f5a140dd0d210f2e646099e91c4b4107ee5182028d32d66a534fa90dd9d45d4b54ee8fb482a9acdb0cf405711140e6944b619d4f185de6be35cc8e69157b876e7c6632c6dacce7ef3f591f1b45706a3d75744d6e6fcd8e193a52eaa273cba7afe60a52bb9c90772a3fb8e4004c51446cf31de17b7cac209e6033be62aef01cd86fe9e53", "keyed": "06fad5dca2d0c1ab104879bfcd86f10b9c2cd16a77cae797c40dd5584fdd65dd7003db899983836f9cbb8144c95d9dd5105fddeb5bd46559b6f89cd597e8a762de0a6814705813434c532380c9fdd6dd086c951a364e32a699f9dea809cade917f528f8ca390763079220b505be258c353f574ed30468a666055d60a7f8338137e57ca4d83b3e1dcbc86e30bb6a0ba69a9e33cee53bab754fc8c0ffcb4bd3592d9e1d6a274bb2d2da0e4bd8c32d34a3842fd5d3ce564d22ddb04bc35ff619f54250edce6c36201fe", "derive": "ceae53602dc8bda3ba5f70e37eac6c52e82380ed108241729c6def03ecb1c958b1dcfbc953368f85f1b95b96573bdc960bfcb1d23f86c7d4d05e05a79a9075707a833227e308173e06714ee3f6a24b2e620ff5daba96b8c63a3777ad6e50d3a6886465c194eb70406b261defad4dcc429813c4c46bae92b2d534561be169c5da0aa4e9b19ab67677cfa3262eab29ca8632484edb8f61bb6fcda32da33eb6bca12d5c5c595866a1e65695ad4f40a3a981e788b442fcafaa9f291e8ad4e7aa38119cff8cfddff5611c"}, + {"seed": 81, "len": 1025, "xof": 16, "key": "25de9eec69b24b07f16d278ec58ded50a93efa7241313709b81091cf0d732b97", "ctx": "lambda-vm oracle review ctx 1025/16", "hash": "eb7596ed1137e1cf9a7c4552bd016a96", "keyed": "36724602bbe0368b2123e429ed365a32", "derive": "b4f1243fc61d26dd1125b33a1c1b668f"}, + {"seed": 82, "len": 1025, "xof": 32, "key": "2838ed52fb997fd9e34ce40ab8e87831dead7c724b4254c8b59b6279a38b2c01", "ctx": "lambda-vm oracle review ctx 1025/32", "hash": "515d83974324f6b2fc3576abb7cb35d5a806079ac79b008183ab128bc687c3cf", "keyed": "17d066fd3a500a6a67ff4219bfb8607ea216f1c9d28373f287ed2c15632db630", "derive": "3485757d5c86ebf235ddbfa8d321602cb09e18dc428ba4170193c70b0f9c56cb"}, + {"seed": 83, "len": 1025, "xof": 64, "key": "4ee4408b9c7f1660100715784b4ce89905fc8f75be8ee48080dd392bfaf97a38", "ctx": "lambda-vm oracle review ctx 1025/64", "hash": "9ab339188c1e0f5503f7f464d22a46de9f6557bf26f6899f16cf6917ecd90c06823978130837cdd82a919b60a800eaaf284a6c910b8f1db578e5c2eb8885eb9f", "keyed": "c7cf6bddc2e55816047dbc1203a4d28ba617a5486b5a6771730edf6ce0d83863fbf07da183fead72816e6f986bd788136b341f97a1067139241acb9f11e736bc", "derive": "7e44d46c1f99a546c9352afd1bc02be2876b27664d1140c424e4cc244b16740dc62524e5a60330ab93d0faeb4fa3f2f6ae1a5e73b375b58fab7eaa9c723f4052"}, + {"seed": 84, "len": 1025, "xof": 131, "key": "9a751b6cc27fef7a0468d503107e27d8abd2f9c7779a39da42c6b0681f0c397d", "ctx": "lambda-vm oracle review ctx 1025/131", "hash": "67377651f2b0f5b916afb417f7fff4e3dece82aede4f89160be3a8956e41343e878a86f2352a5e7502583e1ad82173843897304d421f3e63849148580465a8533525c014f6421291cb028a1a84ac06e828e1d9ce378aaee0284e38e634f2cd9d74320302b4c1e0f165cc6cf90de17dfc9103afe60c8bb9ab81a062093a5d9df14ddd31", "keyed": "96c29d9fe588587498c961de7a23df82d564a8723ac5bc3e64d0bfe96b3fbb5a36bc482b4d9589b1421829b28d846e8df53729010627e3f855eda5e8fe3ca91dd83cc2b32d99e49fd511af21a0edf5eb56db0bed06584bf2c84a78ebfefcf12440af201ea7aef0c7f84068dabb3260ad3fa43eefd4b7015caa4bc55dbaec08916e9fb2", "derive": "ddecdfaf6e4b58a7e29d4508738406a4e5358e2492db8d5523d7ac7b4ec5b93f55d663fabe40000599cf1442159c864420d0ede939ec960b3c9182defd0d204542e9498a7bb769b828b181bcd75e36843db0480da80ee6b0fb698887c6c74c9991ea379564ca97b6e23ba329073fabd11dce75de6fc779f21670abaec2a74b7a8ba8e4"}, + {"seed": 85, "len": 1025, "xof": 200, "key": "2e9f9851066dc5a89e022a1b1fcb9dd3ea3ad37231117f01a9a30c393ea4d93d", "ctx": "lambda-vm oracle review ctx 1025/200", "hash": "561b9a7107a3a0ec41da537e6436d614de0928fd8c9f14e5e4db96469612c0461b5659031dc5b33f8f46b05c260b455cb4b400f1c16af79d18abb9c7ac134fca2d36a9024b31fe9ef4d3050628512ea0f945466b9c7d1ecb2a807d9f7e9c2611882747ed315e6e5236a11fd31ac672d36740ee41ac58db2330bc9f2efaffe5ee5ddf2253436013fe604435128ec434422b166e7b64a87c14eac7105e52e2c09c3fac5fdea6b3416ac44e29252c4dfcf4cb873488493a1aec0b9fa6ebe222744414c5982a6878d4da", "keyed": "8d7ad356afca0147cad9a7586babfb2e699d00d42c1dcf1342510b995beb3330ecb5d7f65a1170f325b0ab8a598fd3cc1d93e300a7124f032a5431255e931c56a0a90f67904d2996f01497d9eb3655dfeb7b55c44e3dcc3c41165d8c8608a51333a9474b6aa6c0c3622b5ee2261757381d12edd67939162ea1502ec17b94d9dea4ad33b42d8381591e1f91bd28f4f0b97f339c8d9e8ec07263c0e3bd12eba080fc4bde37413ecf9b7ba98ce52eb130185e219b826d8c747d132f58df61348f616fab68d72786be98", "derive": "0cc6758101c11c1e83655fe193ba15974da360aa10103233f08e46dd8e6dc9775ab0547d2ceaf644fff8b0601b04030793416915e97a0f0e5f0e4abf9a1d21e3d5b2ca252ec85be73fc26b22aa6f226eccddc2d7371498fa4ebe2e8f25f927fe57d6d4bcb7ae772146256a59d05e77a9e6e4710dd5f42605b0ca29122b9832b07e789c79806ec493a47313a088f17e30085f4aee3d90b7455a95f2024ebd00e096766724239a45c4a506ec3bc4ad44d1ea9a0e77921026537aa737350ae1a316c4df190b03e72b9a"}, + {"seed": 86, "len": 2048, "xof": 16, "key": "c37be63b7d15d6a397ff96ed27f207d7e29d9a22280c4c6e3147346f8b4c09a9", "ctx": "lambda-vm oracle review ctx 2048/16", "hash": "eccfe2bee6eca100dff3592bae156923", "keyed": "addf3e4da8e55eb85d9f0e38721f1f60", "derive": "be4ab18eeceb4e26cad91450546f2da5"}, + {"seed": 87, "len": 2048, "xof": 32, "key": "57a617ef49735dbededd8faa977a78443dcfa50bf196c96963bfb744972763fb", "ctx": "lambda-vm oracle review ctx 2048/32", "hash": "4749afc7faa42ccaf1222708f798c18a1e11146039f23da9f3b009486822f209", "keyed": "723b806ec4a871554612db8ce1c077f082793feb24af79da0da1a86d43d443db", "derive": "48e7b73f77f56d57dbda815fe5be0409bb668ddc0ef2c9774516ab27eca6b96b"}, + {"seed": 88, "len": 2048, "xof": 64, "key": "9c93f6150956b1b932501635387e36d88f1e9134fa430aa814181522c36d814b", "ctx": "lambda-vm oracle review ctx 2048/64", "hash": "e9e20cb49eab02a1b08dbccf5b09fd3b22cc43d84981865680eefefbae22584b3f53c0b3c9808b6d555e355e3b2fd4626dd5d00c4b65a781c1c7b5499969ac00", "keyed": "11ab1e09e164786a65ae13aecef2165c98fa89ef0567189b0dd8ab1ad6e78c4e2e5196272cdc58f02340a5f25dcad3bb2482c8e6b6e9b9c09fc181067046fbfd", "derive": "0ce71bf1f1bb53dc0c19f775da02b69d0bdd2d355f3c0d1a2fafb45551cafbefa6a15b3a1376a8435565449861c0341d37bc2c7cfc88958fb52b2555d7b707cc"}, + {"seed": 89, "len": 2048, "xof": 131, "key": "c23e6b9cb89f41a0c7d7e7bf22165dec740bac9f63895152802e43d3ac0553ed", "ctx": "lambda-vm oracle review ctx 2048/131", "hash": "dd91ebca42a55f4899cd0e25a0ec102c087d25c1b5c230ac1b5e8a074448720dfdb41736dffff0f5717f6c8e9a0dd6eda746cdc7f7634d4a69bbc82fad49f76ca38f7d58d8aa46728fd78231e9efc5d5249d0a412b2cf9b275c564d24c2fde7d5d968bfd3ea35d981e32d8f17ac66c4f0f29bc4dcd8082f324f1b1d6883ea2bfd5f6b6", "keyed": "457a806e76d666cd198569275734101273ea0c936e6b9380cf85ead6eead97a5f31a936c0bc6951aae3ca46178efe99f4d6ed98b83efb28ec5ec9d660e6b1048d8550b3001e29dfad2772f381b891577aa92e7f7d597411f537b3712bd5db8f7d1c5005699982fdba5bacd5ad66762f7d2b7fe2ec9a6dbc15a715fbf6e0f026100cdf0", "derive": "7819e47d406aeb41c0316d7db3f9d539ae763e768701334e313e938986369bb4297a90be5fbca72cb126074431248f0d570291c5906f71a01a479e66244d9cbfeddf966b83b53f578db9b5deb2568e62616142bef9edc4157c4e9e7fc91d3ad87f6c7340ab7dc493e3deaf6c4beee6278e100de4f90cb16e20c6ee62d5a7541ef9a5cd"}, + {"seed": 90, "len": 2048, "xof": 200, "key": "e8e0fe22f74b3bc9d2796d01e9a5461359ba22761ec3ae62fd250247808873e8", "ctx": "lambda-vm oracle review ctx 2048/200", "hash": "d1acdc026a5436d968dad88e6f04542b8d0df5846ee2a6ac387c44e9295e16df61c66897b623dd45a8033372bc77320274be1b5c38511ee24886b7d53e088f4e15ed3e140f5f99a829c4bfb70b70bdca42565e98bd38e785319eb5a4ee0bf5931a4bd6b8ee0b6e59eec5d9ea8bd792e426a1a56ea56e5545a8e8b65cad9ccf1d9678fd64c38f33879e17d1312359b2a4d314e8ef6ab2f1587e2ceb161bc4552df062416a58474ad3056defce3f2f6e7e7a175d6ecb012bceb91385930f8a47fa557640ce4276bb61", "keyed": "da61c6d28ac5f5f719ab444f8013ad9c56888df92dad2e939fc092438a0e83b17ac0824bea6c015330c7c3f2589a33e58bbb2ac5e14c5477c2b2024e698c47f52ede48d55563dadc46e783c06c24067aad1d8417cf7e2d8fec59444ea700cb6ded3dfb8379c73f39325c8a0ced75d62e995711542170784afba801ac92a14d95161fc109f220b6c120d4797155c3678ad0bbe529ce587250728b26fb64061a06a2ee67f8d1e3492b6a07d338d327ed709c42608bf2781279821cb1b21749203f6f6c9d04fff3a4ae", "derive": "2ed11775a07d4c1b517d354e5c89151b5336dab0588125e8f85990502ef6d85ef3309272cb31f4acb3a7fcf4942de8edc6f4329dbab4a1200d0facca24518edfe06f706e76a95b012e50a33e3f5ae3cdf41af17985317f19cccb7f753ea9bc57c30e0a7f1d0f7fd5234b5fcd2ed8a8ee1d60fd6b07af2b5ebfad1b5aef91c5f6be13d5f33981c9ef7f60a4ade34b9523a0d75bfb3a365e645a849caa98c12836dc0cc89fc2c4c322260a76c6ec499d39bb8ce8449eb3746a875356d5c27ac428513da2f9ae19424a"}, + {"seed": 91, "len": 4096, "xof": 16, "key": "0e8cbffd842407148829d0b971714b98068d3bc8afe1d04b5ebf2742245f5d1c", "ctx": "lambda-vm oracle review ctx 4096/16", "hash": "215da79fcb78be79dab730b78467a89d", "keyed": "17e84551f53d55feccf53cd8752232ab", "derive": "744b75aed4882da612ecabbeed555a24"}, + {"seed": 92, "len": 4096, "xof": 32, "key": "37d59cfec72fe64019e0139c588a3b2ba063e011e4b870cdb1ffe366582349af", "ctx": "lambda-vm oracle review ctx 4096/32", "hash": "2b2db219466f8183cd837cffb20505126a523114d0e2a69ddb425f9501933912", "keyed": "bd68a9f1b0d69d19bd1d0737004df16d3ff1ac827f80aef3804bb59f2d11961c", "derive": "1a485b0f2b1a6dcffa47e71159adceb8eec678d2576ee68d1dbde82c9a00d590"}, + {"seed": 93, "len": 4096, "xof": 64, "key": "cb00aa01b802dfc2eaa2256eed82a0208816629b4e905a9b3090c69e68274d03", "ctx": "lambda-vm oracle review ctx 4096/64", "hash": "0492d1ff2de887b4febea60dfa8ab27f9d322708de806629c2378282b3686518606927b089be53a118bde1205242973aa32eb3c32708cea83aecb0cb5ae0d08e", "keyed": "3939705bf049fecda37a7b3adfdfc277139ec2243353bccdafa11d7a841f662dd49379862bd294a7f3a04567bd9f9a5ba2785e7eff94fc16e5886eaf2439f4ab", "derive": "c71aa99c235917053a09e55bfda0ae9d5214f5faa6d54498f7ee690f6c6a2af0dae3578f9aef9ad1b4fb7af11656f97e95955af8d697b0570e41f18ef5c92e46"}, + {"seed": 94, "len": 4096, "xof": 131, "key": "8223c60b3ec3110cce7438b53f7223e9baa8c3abf2b43bffb5ce0df1b1118ca2", "ctx": "lambda-vm oracle review ctx 4096/131", "hash": "ad5d14700821d7ee7842e21815bde1f0f2c4374b57349b259b3d3de06337db474de14e8bf3d3315a5cec5cffd2a31c8a0a54150feda2b4f7dab19057a1e5d4ba13ddf1ad855968bf33cd77de4a46571f3b0c7869e5ec88ed34cbab8b4c18e2e3f64f736ca9cc06f145138de1ad586c2fbba5fd22cfd5d31c4fcbf8c3d71ab120ec552e", "keyed": "47765f1c959389a8a342f9050feb7d202d51f6c76e718dd9913289e00cac33d09e53dd6a9486f24fbe927dfa921c9e4060e5ec0587ab0eea545c998de6292fadc74ca5874629c9d2e0a85bd2d44dd0967bd6da74103f9b7062bbd3c0153b53f0342db20740e0e5065197a6c56362152649fd106662a2a3136f4bec163472633aedce57", "derive": "a966caccc348466a42226f6ed900ef7031c22eb751422ebc32c904e44dcbf5f5e3dd676a6cc16bbdfd6a87bd91d46c867bd850a864d2eb39be3f946a90b0ece4527838452b18d041b86b5e3b726905845f664afb0fed035d51dfdcb8e907b9a75ba216acd7f2182e34e5b47f65fc1977796f5a2132f4b6bcbc691bc9e7fae805c28287"}, + {"seed": 95, "len": 4096, "xof": 200, "key": "174e6662d2c05955c063523f5288eadb34328c4fa2b27165858897cae98b048d", "ctx": "lambda-vm oracle review ctx 4096/200", "hash": "05420df742c2a977945cd2090e7faf6d969e63a2c6238cc0770e40ac9a86669fe7ff09b94c0b2ceb41ae5ff5321864c1247f5bd42040ab4f02c49330807b307e30191bc6ae242805ff2512d417c847b12fb9d85555052ec43db978585716de1b6246b170d82cb92ac1fdef0b34818b4c06739152d25009f284eda20e493736fa55c688c9e6d6407c648dff4e72caad3b7f51aac4aec4c53b831b785006f0a1338212600b2ef65c3af11fcd729409bca76308a41da0639b6e23737ce70c86426fcc1beeed41559d7a", "keyed": "d9997ec1c3f46309869da8818b3e5f634a48adf8a2227dc3e2eeed9f9bd4b1842e442e1d444a2047e711de7c97bfd1b368f73c0157843e35ee4d46f4662d5eb259765ca7c4d50e57a7b93cbc7e2f18a085b083315782253a60049b5a1e5aafaa7f425ff81afc5f306ecd7306db3060d16f7df63e5dd81ea43fbee15f8c6d2198c1a001a576538bf22e489d1d812604c46e2dd6cfc405bb5241ac80b3dd5c0ef9e778daa460c3ad084bec4889c6a0009d200b765e131074781a0b247cde5700fc04d46a6fc1b019b6", "derive": "ec2762b33af35e89aa4c07c733054536582a31fb630bdd6fd8c25aed6ba6b36f1e66f577cc2458342d87c56474f9abeb6c81d2666fa1e9342d01abc12e35f1175395247bd1e654d74797bbd934424200de4f735307cc79fb0d640111559771911f0adb0ee081880b6a72c3281f6c2827453ee54c4cdd64fd419237353a1da8e6a08046c26b833407830a767febcc65e500e90195e13601285b4109d54c0d94eb55f15f3846b2b92dcc2e9ddc6d7f0232e50c151db38fd8495882764456e15893237c77345d5d95db"}, + {"seed": 96, "len": 4097, "xof": 16, "key": "69567c30361e6b67e55967d288477bf78c8e0af84fcda6abc9258567e858c90b", "ctx": "lambda-vm oracle review ctx 4097/16", "hash": "867aca04bbac9c1d3a07278138e9ad1f", "keyed": "38e03b39b3e55beb53bdd77ce350c832", "derive": "4753c2c7254782c32be47b7a410f4572"}, + {"seed": 97, "len": 4097, "xof": 32, "key": "8f930a77102fa62bb15ffc8cce6ff1314dcc31eb20f2d17050b09fc6602f520d", "ctx": "lambda-vm oracle review ctx 4097/32", "hash": "ed871b6a5ee95e25f03e981e5bec7758ec00523f4986852510db5a4162961c85", "keyed": "9d5efe1b892cbe1ae7ab8a04f0f56fd4882d0477a34a655b48218fae1c83165a", "derive": "3548c2cee59bbc4fdc4266169ddf3464dfdded393b0066bbe7a33490dd1107ad"}, + {"seed": 98, "len": 4097, "xof": 64, "key": "927ff7293c0dd8b741106fbb7479028a1fce6b6fefa6f6266296e989358c56e5", "ctx": "lambda-vm oracle review ctx 4097/64", "hash": "b6e61602cee007c2cc998ef402a2a50d02cc1b988dfbe7362139f06226f533e8f9556a1ec8b0ae749c7761da36cb5a97663eae8a13b13c7fd4ba39140ea12e1f", "keyed": "38390cedeb4636357acffbb91d85265c1d32f872c55e5e9e341e8f581df4e23e8823fa7cb5e239c857368bdd2c21984398d3662393910e2612f5c2ab1fe61780", "derive": "0f4b6becfee37bc9612d188bc1d80b5ff3f81396ada402e0daa5dc899e9e55c6cf2dc5cc759e77e0ffaa7eb26ec5785b1d589b9ddb14fab4963002670452194f"}, + {"seed": 99, "len": 4097, "xof": 131, "key": "b8bc393f6cd619e9b5748cc48c90ca3bdc39bf0ab4d2d43c502a939bdfbfbd56", "ctx": "lambda-vm oracle review ctx 4097/131", "hash": "c5e5938ad2e8304bf0d5bda578ddc94dfedc903f9d6cc4401b8b443ebb8cb6cf21ae9e8c453a5ea7d1a6e50c04995bbeb9bcedf458d0581d9c0ed2845d1c9bd9163da1e26478341d619b4aa441211ebad0344f44ad621d211e9b6295c9d74a121707802ec788a8f453c090cb6da6e95be787afa0b1c3efb8a82a0badb816919bf7be19", "keyed": "def95aa4c1ff157d04ff55cbd23310031603969f5b45decd791bb0ff5064643b6a31a69a16de293af9a7fe719c60cc5e3bba6c8bf4ebb04705da1ef8caba76635f84dffca60164493bb5beaec75224ec7a4b0818d4ad88ba3b8efdb70b52d10e66418757457e667b9e3239b3a5dbb8c6dd823dc93e0f3d0b7ee68736e010ecb271bafe", "derive": "68ae58a3f9156bf80acbc356843f6eff735adb807ef7fca62f034d02e85111f26b043baac74a36dda7531c8d24406dde16827c0e0dec91ec969d980e09ee2582b8e1b4f693efee008e9ff2f28fe8cab0b821b1a4dab4fb24813c09dccc3d195e3af888f9bb6e3487b53b3607913e9381e83e9c66784a7e1586b46e83681a9f8d2903e3"}, + {"seed": 100, "len": 4097, "xof": 200, "key": "4926ffd129f4d4da38a0464c3cd5ae104e540c96a195dd522573610ae621345a", "ctx": "lambda-vm oracle review ctx 4097/200", "hash": "b4b19f35b3eab71e25b86a0d12f234756d79531e32ea6b9ad70c9f275f949353ac8e6c18fbe4124bc16a572fdd3c1f89243eb10c5e0e86b4d344148513c3f802a8544d87bcc9aefa6a318602cce20fcc486fe7f265769cd4154a7b978f6e1d76946927f67af6967aa4906f74d09047fbdf203379d6f9eadff3612ac884b6839852a79716bc4bce6f7853325202b0fcf23573f41af4f8de558f9e62eb5268dc8712f7786194118f81694cdd73bc4d5153ae6c32fab2f35d5caf5f9c8d6ac0ee4ad0458f56c132efa4", "keyed": "ffdb15a8f97a8e8f27d1e6fa9f83b862bce80f164f894aa56a330e5366db864787fe52d1ec92af5ff062bb1f12564e8c004be44e59ae8734b131d609da13d6b915b1fb4c954afda7ad5ab0bcc086f203c03e05232d2be549ef0c73243418f249e31d68c2a022bd6a140402c444514213e8060df9d96d50ab7e10bc40e2dcae2506d4fa910e3639102967b8671489589b156068a28089dc5d278deaf7045ab64ff84c237245fd98f8951a5b17fe7de46a48eca62f6862cf986fa808b4ab8a3a0fbe3c65d49423574a", "derive": "c9bccf719c967863b07ccaaab3c44c0644af36192e6f7091680ffc5d2a36b4847082068e42de15377d55b4dd06ce66e60294168989306f12183933cb1e86289aadf85d7e09e3b6b71264be605b1d7774fdc4741c3b569362d2e158088430c1985a8eb41dde271c4d53eac15b4268794c2bfa46032d2f598e0d02f897b9767379b988466ecfbb7eecd9b9621c73ddc78e7017f63189aa06441594adf632bfd439ec66f98ab49d2591e0fdc906e7006ff057f149430f756ee06ab323506921f9552a475bf61d5ae158"}, + {"seed": 101, "len": 10000, "xof": 16, "key": "debf6bda8ae84c02678eb007feec1ae5037fe2fdd886c749bbd8ba07fa751ae1", "ctx": "lambda-vm oracle review ctx 10000/16", "hash": "298af4e1235a1a2cc4b1f7e6371e4296", "keyed": "985164b642278a61f7885477fcdd7d4a", "derive": "9edd4f3905cba501cf38174df53cfa57"}, + {"seed": 102, "len": 10000, "xof": 32, "key": "724ee3cb2af83fd3c7fe1479201facaef25de4d47ab374b04d51dc2a7dd2414c", "ctx": "lambda-vm oracle review ctx 10000/32", "hash": "8797cc68e0e8f52c95e0e1c805d3ed9b66ccee9b629f9c0ad014faaebd96877b", "keyed": "08bfaeb67847f9d8da30caf3daad2c4dd763013166dd954e33e001c5fcb38543", "derive": "195f9d3c9d30f5dec592cc0a81ed4d71c5bfe2b647ba8b2dbe48877e6b8ecc56"}, + {"seed": 103, "len": 10000, "xof": 64, "key": "07e8bda29e5416f49e9edd67d44e6a6eb3604796baa70a54c03d6db1b53a3246", "ctx": "lambda-vm oracle review ctx 10000/64", "hash": "691f34c88445969efaa89be227fd360393e3b9fd66075583c7214d3a22685894b5d5779035a9b3114de3e8c88c48d12f28165cf3b921f933e9758116f46b2cc6", "keyed": "8736e42e82ac72ad5296420d9b8ebdc1f7f16be14ee45f0f98c8bd608ad9dd878ed5a0794997ddf234b57a44eb56233bae99087f5de09bcab7b7256865fe77c6", "derive": "186867f6ee0a0a30cf90e931c687519c5c6015db1855bec7f687c0d14408f3f2f6170bb892a8104814730b5616eae4185abeb2184e4805c84304ff633871da89"}, + {"seed": 104, "len": 10000, "xof": 131, "key": "7cd1fd085fe8d82385a78adc0c2fb6967c37bceaf58c1ca36f75ccf94597297d", "ctx": "lambda-vm oracle review ctx 10000/131", "hash": "0a31dc0885cb9f01f608c472c927642bfc6da83faaeb3cbd08df693fa970e32fe63752f15ad7b44acc7aa84184f57f230256307e0e19d41bd7045b6e0acd2e0f1030d59fce33e6ec690eb1d61123b629f661af35f5b5289599a8c610e91954dfcfe2e10c51e989d2afac0f137db9cd9ed349bc9c268ecdb4de985890a780f9b4583491", "keyed": "789727759c1fa74d67fb10747ea2d6b63f880fa2c65384c7001edaa7a036c6a20e517f8722f41cba69eae3250ba124c6d5223382c7d7e715d12896091d4e06836afa08fb946a6e4095caed92fa1b32bf184170e17bd993664b243e25b4bad23b1fd502903ef89df669f42a33d3b8ecc0ae6e34c1e36d0fc30a205fca676b546bacdc8b", "derive": "6d23542e2401bdde33d6cfb6e1d5e711012e509a4d13075fc42b0f1f263c81807285351fe8fce9e755d45ebdaf1abec13e3031102472b775fde583672313104fbda0636b15485f925ce8a2a2bae14c4d7d9e4a595c1733c5e5017d9a54b67fa8238e43079b9ad609c0cc27c0bedf6bad841f8bf9f37cec048d6f99244df1e0c1e8bec2"}, + {"seed": 105, "len": 10000, "xof": 200, "key": "a20d1c6d3a8c8000132928d6d8de2d7a01b4352deb394230b46fe47a9d6d2319", "ctx": "lambda-vm oracle review ctx 10000/200", "hash": "de90c25b7c0cee636412502de2183d0f0ddac7238f059d540335550a92507c51181c3f525fe2072f17208a06e035a6b090c67184f792915181ff35b6986008ec6868104328fcc6f451da8478998ac6e310e4ce86f1a4356f839da5d21c44753a15486730c13028dd542d70ccbd220e32b4958e93845b6886937e7e1d2d720c3cb748876318bc9aecfcd7b7c2d66af44abe5757db8f0bc8eb697c1b6b4844d95569e295128700cfe2567def2ea7b13bcb53c315d020d9d9cf4abd77e4e242c16533fca443eb3aeeda", "keyed": "d5322c8131d3f7490f9b61b92ace6f127e372344af6acd440109be79ba3f3bbc0c9e7c68d251f1f6ba23b93d4f3c1e2bab8fbe4a6fabaabe17a27f12424117db0f4d24ca91ced9744601c9eea0083f0d0fa234af5ddd38bd316971f12570654c6871fb53edff0b2d2cc268a5898782b4e22c392d4b933e4f9120667caecb8c5f78bbc15ab143a97d13841dfebc3b7097569180a12b6d02350892625fab5bb723aa170c42da7f6213c005e76e82354ebcc2aa4074020000bb14a94035835748675a9ad1dac4d21abc", "derive": "49a119c6297ac0cfd9282cec209071d6b269b77b20cf1f32d700bb29d0174e65bd8149052f31efd0a6b1f9c902a6e8994ab821a84454877c412a7c8830219c8dd6dc0143accbe984687600e7e3b73f8c18929e98f434e8d151bf8ef849f3c48605d840a29065391ad342dcbd267e046d3558254f443eb539bf5878eb71721fed08922869cc7ab9d21218ca393dc300e970d0f7c0dcb15953639639c1ba29ffd6b411a297dd3288476d11c832cd326636d67ccacf258b48658877dc874594ee33163abf82196c02ce"}, + {"seed": 106, "len": 65536, "xof": 16, "key": "c7fcd83f19782a915cf9d246aec8c8ba5356c0236f5aab21745ca8706cd378e5", "ctx": "lambda-vm oracle review ctx 65536/16", "hash": "250e73c83ef8c2547bc66cbb62db7cf9", "keyed": "68a9254c8bca25432f39914236af4697", "derive": "c2610f8c15910689cc8b9071b3e618b6"}, + {"seed": 107, "len": 65536, "xof": 32, "key": "ed3888f753dca5e6740f4e89c20c13885e463030c76298c6c0cad9343166d9ea", "ctx": "lambda-vm oracle review ctx 65536/32", "hash": "d987ec830797a4b3c39f7b2da883106acde4dca8088a0c61c9ba75efb3db9f88", "keyed": "34b1c21894a95a245a74e0681ed5b38774e0b282adc477bdd1c58f3f5898f75b", "derive": "95b688c7d8a773cd6ed197bdfe9c3ddfcb3983c4b8a0aabaa36e1ffa6f21ad95"}, + {"seed": 108, "len": 65536, "xof": 64, "key": "5ca03baa8317d67cc39f60d104b35d1aef8869c74baccea4341bd825d246e351", "ctx": "lambda-vm oracle review ctx 65536/64", "hash": "32b03bba7802ca730ca05939ff1e1ffdec0a09af285130a4cdc148ef81d78eb98baa097a8ffad1e4b876f0bb59a18c7e2b45c8cb3813898e2403984f7a6c6071", "keyed": "3b0a32ca1166401b8d712f829dc16688095153d108912de3c8414e0a6750d6d0497d72fe25247c8d7e0e6e9c9023880cf0752310a7584748dc31891bfd9ef33c", "derive": "82899fe0ac33c8eaabc4b08dc0094cb450d9ccf0aebdda852eaab18adc4ad343d8d638e331496ef18fc23dfa48e3ce3cf77b3ad76403353d244b48e617b44da6"}, + {"seed": 109, "len": 65536, "xof": 131, "key": "f03938cf2f49ac7e99fdc4a957b6b3e99cc5288c4feacc5837526a08f35f5ad4", "ctx": "lambda-vm oracle review ctx 65536/131", "hash": "5ec02bd02ee4d382db8d10d0c4a2a014087558869944c60b97531eee05c2a5ee5fb8664fbf041bb64f09c5e997bd8a6db7dc18ecdf22285fe7a1ad368df8b0683e37c328cece89b0e90992bafc70f7987bb064338dc4cb025cf366aaee42c7e8fbcb9fe28a816b97c1766c7184bbdea668fe11db1f6aab31f189b28f9f4014a24b17b6", "keyed": "21c7d240682985db03161379efff7c36d04594c5a5b166b345a54a35f6fbb6c0c60a202e34a71ac97fb7f9888c86dfe66ef0781888245c332a45c7ff494385f6e7c950a1a561ab98dbb352ce4f9ea6fabb1684ac152ef00cb7d03f6811f7faf424e0dd4cb24de541a2a4762be4d2f76c22f752d18942a7c8961d0c99221da20a0b81a9", "derive": "f1c0de3ac81f9e3907a67c39b1a09949da0ff0561cfd84c6878cc7b969ee7cb89949bd0a4d217c7780f038cf7659a2e8e9f8472d9f414a3870e5adfecd48ecfe0187225bd5acffd898b25cbf7faeaa3afce321b2add22e62459e907f99843fd689ecc0e448c02b4a389db38c8fc52ca0f978e727077ec4dc58e5a6521ef07ad59a3c5a"}, + {"seed": 110, "len": 65536, "xof": 200, "key": "a7cbf3e166bbab1632153b7b7a9053d6b42f4d853b55e21867323ad5b55fcb0f", "ctx": "lambda-vm oracle review ctx 65536/200", "hash": "9d3608bf4225fd301cb08dbb27d22bdcd902d833ee5e8896e535d799c2ec10129c8aab47a6602262ad17b3589b66ee97047f29ab1606de811d00e3faa6bbfefc54afef9dbd61ca79675d3d583a819fae4e8adc0307da8e5e3202e2b25cc78af20e13d73a16656edb5ade79f77163d748813776916247f627452123b2aa46910a7fb7706460e8f72aa5bcb6f2685819960e6c85ce66ee560daf963c3afbe4a21e8fcf5750fcac93f62f13bd273d02082aa462f03497e97d0fca5a1b39c8a6fff4bd83aed05525c5e1", "keyed": "9b312955f025b12c33f0fb6d1a7724847b6bc5416bc8a7545b5b1c40e95047c479157113b57c0a4cc2d1acc5f16643f22285499f06f211c4ba208d9c2969e3a2bf21ccb3b1808ce309bb887eea150047a0c4e7ea981a57f7fbffbcf596ab616865fb3bdf428e77d1ec60e52a1f7f2f4796d13edfcc793ff7a5973793537793a902f315058d0c3f0a67968ff812657d46585c063165ff311c7ae19f218dde369cd5e89885ba08d2f4d5bb96c00ebc3e33dbda71fa8a873cbdd2c26b102aa49248fa4177f958fe5ce1", "derive": "8772e113081fe301b461adf0a1fbf105a199eee3d0c0f27836662a6ed7c62c01037ff56c18b298028babcb45aa1c7f13335f9fb8dc2f5931021be53ef21da4727b20927d3860eb17b3dd7ee51aeaae18aea989de6b897baa14547cb3e6c1bbec9bc63e66a078cac93815c4646e5690fb7ebbeabe010e44c9dd59eb2b5e6399b24c66f9d859390e170e94e45fe942d230788b9abce7392a3efe7d9652ca5e94cc63996cb47260dd9111864415368f206513609d744a807e7a2fd044b627d1ecc9b317bee514aaa7f4"}, + {"seed": 111, "len": 100000, "xof": 16, "key": "3b643c5a1b5e239a0799eaf8ff13931b82edb8fea42a47caa825b0244d4b7b00", "ctx": "lambda-vm oracle review ctx 100000/16", "hash": "dafc038f963a4fbb56a970d51433e9c7", "keyed": "ff1fe75b79c283887239712536a6f8b5", "derive": "4ca500d55e5ceec2fe5c02021403c4b5"}, + {"seed": 112, "len": 100000, "xof": 32, "key": "d394a71b890df7dfee174b95ed22722c143195fef201d138f2a8df894d16f144", "ctx": "lambda-vm oracle review ctx 100000/32", "hash": "58d738cba5b0b79e4d6f2035fb41acf271f1fed88b8e432a51958f43e827a212", "keyed": "6dd9792a242527b2d3013515dc5a7c75e36ddb6df6a6d41b373533f753c09454", "derive": "82f5c674b0ae22e41dd9424c64f66d4ff4ca3bc67ab20f0fcee1a97dfb6901f8"}, + {"seed": 113, "len": 100000, "xof": 64, "key": "f93f8bd6a30919f2178dd9860e65cc1da2d5a23e6dfda14cb14a2aa8e181732e", "ctx": "lambda-vm oracle review ctx 100000/64", "hash": "bf0c93ac003d8e92e511fe59d3309141fc1bc993c38d9a63737e4f508d68040f9422080ad44bb74dcb578d37bafb82ab021f758e98d347fe407576bce315f86d", "keyed": "90f3212d27f2a1919d209a19754424ff0b2704d7480d84ed882cb5fad926e7229d8d92a2b8daf1bc7b17e0333098265e7caf0eae401e0cbfcb898421a9245553", "derive": "7829eeda7bd48bb51b4b28147245a7bafb2df37e018029598098c99797cb174d2526858c341bf54a8d90eae6240572e0e590653c6486c5413bcbbdd390af9a25"}, + {"seed": 114, "len": 100000, "xof": 131, "key": "fcbd4502b4c333ae50c954a138f4b41913588e1d5e966ab5580f9d3c98d5bf28", "ctx": "lambda-vm oracle review ctx 100000/131", "hash": "4d1c5f62bf411a2b6f6711f4d9913c0669cc472b231099c41c7f9a45343ec421ac727a010b1e495a4311defea2baa88141a9dd60192a4d97a06f4818a527b4b65c322b4554b4a0c29fdeacd4fec1ba1afa2e0abec8b577c84f9417c376b2472272ac055878b0d42e5f9c4dd4939f27a4b46c0ef080c2e2abefe3fad3abb728c067cde5", "keyed": "bfa9973a8c02b8882e1450dd434a4b9d7072c877d8a2f94346fe06dbd5abd96d8fee4ed82aac7f3fec8addd2d7b197f030ea5322dc7d4217815a0a1f73987818ea935d09fa8f1513a8c18d87f9ba5f0f6a7fb57ab90c01c6aa8fa5aa59b304ae1240d0bcdd3e670786033ca4137ab6968a3491e2ebcddd638c633bbce0488b54eea92d", "derive": "a64ef45282602fb582847effffdddf218568cf916d38bab6cf711e8ec7569cb54d1ae624928ef8476942c2958c9d021e72ac012bd5c28cbbbfacf2239d968e783b310f23f6301eed08402d08078bda9da21a46c41c9728053e97b6d964f55aa3c8cfbb8698d6ef94dd2507ead85e454947d8bbea81159162c16c25134c0c92b519c0a7"}, + {"seed": 115, "len": 100000, "xof": 200, "key": "2268978ce6bb8db3b5133d660df23099096d6778ee1946a59d217165c34429a4", "ctx": "lambda-vm oracle review ctx 100000/200", "hash": "9d0a9882067f0f7e8adab6330d9e62498da7623daa31f39e984189b42830d444a4c7ad037fbe55db999626c6ccafb1737145a2ce28c63fa0380322bed696938395f8e18cf7c1ec1d032a81f044e4bd58a2f8b930b9f91fab466dcdbb97825ce61b696c1d9af7653380ca80c4ad3224338b2bb42d420c7d44d2d42e2beb9bd6e5d3f714df6dcd651c2ca655601c3ff2d50020df05b9f8c60617633f6f0648d4f57d1b98b9ff861dec6e0e3ee494729489d7b81f587fd0192fb7d93e25e28ca359967f6ce919f1ffa3", "keyed": "e6c5d4fb218f27ac862338bc0a6a272336d53ef38d415584fefe7989807ee222a683a3222031a9839dd5f02cd61687e9c6bd68792308dd86e43b3daf159defc21ca8f7663d4d8ce2c3c80539f9b9eae2656a4b299ca44d94381be56720599b799fef8e674e83f8114ac14f180deb55bdc0e3527fc2b69492540aa28dcf84f56ba9dcc89225122a0f7918b8892ee3afe40b24335ddb84a2f92796819602d8c88085cb77f45228cd7ed23d5b323c1bc7601fa85ceb1f9d57e541b834633ea7f5af70d52fcb3cc1fe4c", "derive": "1e6b77b3cfd3bb00000c04a2575b772c18720102877b8cc95379163219e67e03f3881cb7e78b893a11f19254cacf00302d68f8032999d7352f132341bb3a58eb002dfbc27dbd09694c82a8f34d006d1479508184954d36f58c821cfb7ae6965f4cce34037777213a433705a76390f33f6e5c80c4276dbdc1514a7e26437c5f490d5ec20b429016bb79d6eedf9b244694f6bd35cdc214fc27662b3e34bcc5ddee99c89fd4e4c443ad5f52958f64a4bc3514a4fb3b6bb4ec3e738836b2e03f0f096b98fdd9f91d1a43"} + ], + "known": {"empty": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262", "abc": "6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"} +} diff --git a/thoughts/blake3/blake3-oracle/test_oracle.py b/thoughts/blake3/blake3-oracle/test_oracle.py new file mode 100644 index 000000000..24eccef98 --- /dev/null +++ b/thoughts/blake3/blake3-oracle/test_oracle.py @@ -0,0 +1,364 @@ +""" +Validation suite for the BLAKE3 compression-function oracle. + +External anchors (independent of `blake3_ref.py`): + 1. Official BLAKE3 `test_vectors.json` (authored by the BLAKE3 team). Covers + the whole-hash output in all three modes (hash / keyed_hash / derive_key) + for 35 input lengths up to 102400 bytes. Passing these exercises the + compression function under every flag combination and many counter values. + 2. The official `blake3` PyPI package (the reference Rust implementation), + differential-tested on randomised inputs of many lengths in all 3 modes. + 3. Plonky3's independent `blake3-air` compression (ported below from + others/Plonky3/blake3-air/src/generation.rs), differential-tested DIRECTLY + at the compression-function level (flags = 0) on random (h, m, t, block_len). + +The 6-round variant has no external vectors; we (a) show it differs from the +7-round function only in the round count and (b) emit 10 canonical vectors. + +Run: ./venv/bin/python test_oracle.py +""" + +import json +import os +import random +import sys + +import blake3_ref as ref + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# Test inputs in test_vectors.json follow a fixed pattern: byte i is (i % 251). +def pattern_input(n): + return bytes(i % 251 for i in range(n)) + + +# --------------------------------------------------------------------------- +# ANCHOR 1: official BLAKE3 test_vectors.json +# --------------------------------------------------------------------------- + +def test_official_vectors(): + path = os.path.join(HERE, "official_test_vectors.json") + data = json.load(open(path)) + key = data["key"].encode("utf-8") + assert len(key) == 32, f"expected 32-byte key, got {len(key)}" + context = data["context_string"] + + cases = data["cases"] + checked = 0 + for c in cases: + n = c["input_len"] + inp = pattern_input(n) + out_len = len(c["hash"]) // 2 # hex -> bytes (extended output length) + + got_hash = ref.blake3_hash(inp, out_len).hex() + assert got_hash == c["hash"], \ + f"[hash] len={n}: mismatch\n got={got_hash}\n exp={c['hash']}" + + got_keyed = ref.blake3_keyed_hash(key, inp, out_len).hex() + assert got_keyed == c["keyed_hash"], \ + f"[keyed] len={n}: mismatch\n got={got_keyed}\n exp={c['keyed_hash']}" + + got_dk = ref.blake3_derive_key(context, inp, out_len).hex() + assert got_dk == c["derive_key"], \ + f"[dkey] len={n}: mismatch\n got={got_dk}\n exp={c['derive_key']}" + + checked += 1 + return checked, len(cases), context + + +# --------------------------------------------------------------------------- +# ANCHOR 2: official `blake3` PyPI package (reference Rust impl) +# --------------------------------------------------------------------------- + +def test_pypi_blake3(): + try: + import blake3 as blake3_pkg + except ImportError: + return None # signal "unavailable" + + rng = random.Random(0xB3B3B3) + lengths = [0, 1, 2, 31, 32, 33, 63, 64, 65, 127, 128, 129, 512, 1000, 1023, + 1024, 1025, 2048, 4096, 4097, 10000, 65536, 100000] + n_checked = 0 + + # 2a. Default hash, default (32-byte) and extended output. + for n in lengths: + msg = bytes(rng.randrange(256) for _ in range(n)) + assert ref.blake3_hash(msg, 32) == blake3_pkg.blake3(msg).digest(), \ + f"pypi default hash mismatch at len={n}" + xof = rng.choice([16, 32, 64, 131, 200]) + assert ref.blake3_hash(msg, xof) == blake3_pkg.blake3(msg).digest(length=xof), \ + f"pypi XOF mismatch at len={n}, xof={xof}" + n_checked += 2 + + # 2b. Keyed hash. + for n in lengths: + key = bytes(rng.randrange(256) for _ in range(32)) + msg = bytes(rng.randrange(256) for _ in range(n)) + assert ref.blake3_keyed_hash(key, msg, 32) == \ + blake3_pkg.blake3(msg, key=key).digest(), f"pypi keyed mismatch at len={n}" + n_checked += 1 + + # 2c. Derive key. + for n in lengths: + ctx = f"lambda-vm blake3 oracle test context {n}" + material = bytes(rng.randrange(256) for _ in range(n)) + got = ref.blake3_derive_key(ctx, material, 32) + exp = blake3_pkg.blake3(material, derive_key_context=ctx).digest() + assert got == exp, f"pypi derive_key mismatch at len={n}" + n_checked += 1 + + return n_checked + + +# --------------------------------------------------------------------------- +# ANCHOR 3: Plonky3 blake3-air independent compression (flags = 0) +# +# Ported directly and independently from +# others/Plonky3/blake3-air/src/generation.rs +# (verifiable_half_round + generate_trace_row_for_round + feed-forward), which +# hardcodes flags = 0 and does exactly 7 rounds. This is a SECOND independent +# implementation of the compression function, checked at the compression level. +# --------------------------------------------------------------------------- + +# Plonky3 constants (constants.rs). IV stored as [lo16, hi16]. +_P3_IV = [ + (0x6A09 << 16) | 0xE667, (0xBB67 << 16) | 0xAE85, + (0x3C6E << 16) | 0xF372, (0xA54F << 16) | 0xF53A, + (0x510E << 16) | 0x527F, (0x9B05 << 16) | 0x688C, + (0x1F83 << 16) | 0xD9AB, (0x5BE0 << 16) | 0xCD19, +] +_P3_MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] + + +def _p3_permute(m): + return [m[_P3_MSG_PERMUTATION[i]] for i in range(16)] + + +def _p3_rotr(x, n): + x &= ref.MASK32 + return ((x >> n) | (x << (32 - n))) & ref.MASK32 + + +def _p3_half_round(a, b, c, d, m, flag): + # verifiable_half_round(generation.rs:203) + rot1, rot2 = (8, 7) if flag else (16, 12) + a = (a + b) & ref.MASK32 + a = (a + m) & ref.MASK32 + d = _p3_rotr(d ^ a, rot1) + c = (c + d) & ref.MASK32 + b = _p3_rotr(b ^ c, rot2) + return a, b, c, d + + +def _p3_round(state, m): + # generate_trace_row_for_round(generation.rs:120), state is [row][col]. + for i in range(4): # columns, first half + state[0][i], state[1][i], state[2][i], state[3][i] = _p3_half_round( + state[0][i], state[1][i], state[2][i], state[3][i], m[2 * i], False) + for i in range(4): # columns, second half + state[0][i], state[1][i], state[2][i], state[3][i] = _p3_half_round( + state[0][i], state[1][i], state[2][i], state[3][i], m[2 * i + 1], True) + for i in range(4): # diagonals, first half + state[0][i], state[1][(i + 1) % 4], state[2][(i + 2) % 4], state[3][(i + 3) % 4] = \ + _p3_half_round(state[0][i], state[1][(i + 1) % 4], state[2][(i + 2) % 4], + state[3][(i + 3) % 4], m[8 + 2 * i], False) + for i in range(4): # diagonals, second half + state[0][i], state[1][(i + 1) % 4], state[2][(i + 2) % 4], state[3][(i + 3) % 4] = \ + _p3_half_round(state[0][i], state[1][(i + 1) % 4], state[2][(i + 2) % 4], + state[3][(i + 3) % 4], m[9 + 2 * i], True) + + +def plonky3_compress(chaining_value, block_words, counter, block_len): + """Independent Plonky3 blake3-air compression. flags is hardcoded 0 + (v[15]=0), matching generation.rs. Returns 16 output words.""" + cv = list(chaining_value) + m = list(block_words) + state = [ + [cv[0], cv[1], cv[2], cv[3]], + [cv[4], cv[5], cv[6], cv[7]], + [_P3_IV[0], _P3_IV[1], _P3_IV[2], _P3_IV[3]], + [counter & ref.MASK32, (counter >> 32) & ref.MASK32, block_len & ref.MASK32, 0], + ] + for r in range(7): + _p3_round(state, m) + if r < 6: + m = _p3_permute(m) + out = [0] * 16 + for i in range(4): + out[i] = state[0][i] ^ state[2][i] + out[4 + i] = state[1][i] ^ state[3][i] + out[8 + i] = state[2][i] ^ cv[i] + out[12 + i] = state[3][i] ^ cv[4 + i] + return out + + +def test_plonky3_differential(): + rng = random.Random(0x9110C43) + n = 20000 + for _ in range(n): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + block_len = rng.randrange(0, 65) + mine = ref.compress(h, m, t, block_len, flags=0, rounds=7) + theirs = plonky3_compress(h, m, t, block_len) + assert mine == theirs, ( + f"Plonky3 differential mismatch\n h={h}\n m={m}\n t={t}\n " + f"block_len={block_len}\n mine={mine}\n theirs={theirs}") + return n + + +# --------------------------------------------------------------------------- +# Internal self-consistency (NOT an external anchor): compress_cv, feed-forward. +# --------------------------------------------------------------------------- + +def test_internal_consistency(): + rng = random.Random(7) + for _ in range(1000): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + bl = rng.randrange(0, 65) + fl = rng.randrange(0, 128) + full = ref.compress(h, m, t, bl, fl) + assert len(full) == 16 + assert ref.compress_cv(h, m, t, bl, fl) == full[:8] + # feed-forward invariant: output[8:16] = v[8:16] ^ h ; recompute v to check. + return 1000 + + +# --------------------------------------------------------------------------- +# 6-ROUND VARIANT: derivation check + canonical vectors. +# --------------------------------------------------------------------------- + +def test_6round_derivation(): + """Confirm the 6-round variant equals 7-round with the loop bound changed, + and that it genuinely differs from the 7-round function.""" + rng = random.Random(0x6) + differ = 0 + for _ in range(2000): + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + bl = rng.randrange(0, 65) + fl = rng.randrange(0, 128) + v6a = ref.compress_6round(h, m, t, bl, fl) + v6b = ref.compress(h, m, t, bl, fl, rounds=6) + assert v6a == v6b, "compress_6round must equal compress(rounds=6)" + if ref.compress(h, m, t, bl, fl, rounds=7) != v6a: + differ += 1 + assert differ > 1990, "6-round and 7-round should differ on essentially all inputs" + return differ + + +def canonical_6round_vectors(): + """Deterministic canonical vectors for the 6-round variant (fixed seeds). + These become the variant's reference going forward (recorded in ORACLE.md).""" + vectors = [] + # 10 deterministic inputs derived from fixed seeds 0..9. + for seed in range(10): + rng = random.Random(seed) + h = [rng.randrange(1 << 32) for _ in range(8)] + m = [rng.randrange(1 << 32) for _ in range(16)] + t = rng.randrange(1 << 64) + bl = rng.randrange(0, 65) + fl = rng.randrange(0, 128) + out = ref.compress_6round(h, m, t, bl, fl) + vectors.append(dict(seed=seed, h=h, m=m, t=t, block_len=bl, flags=fl, out=out)) + return vectors + + +# --------------------------------------------------------------------------- + +def main(): + print("=" * 74) + print("BLAKE3 compression-function ORACLE — validation") + print("=" * 74) + + # Each anchor is independent: a missing fixture SKIPs that anchor only. It must + # never cascade — a FileNotFoundError here used to abort anchors 2 and 3 AND the + # canonical-vector emitter below, which silently blocked the z3 gate's positive + # controls on an unrelated download. + status = {} + + # Anchor 1. NOTE: the vector file ships regenerated from the official `blake3` + # Rust crate (see ../ground-truth/), not downloaded from upstream. Same official + # parameters and a non-circular reference, but not the published artifact — the + # label says so rather than claiming more than we have. + try: + checked, total, ctx = test_official_vectors() + print(f"[1] Official-parameter vectors : PASS ({checked}/{total} cases x 3 modes)") + print(f" modes: default hash, keyed hash, derive_key context={ctx!r}") + print(" source: regenerated from the official blake3 crate, not the published file") + status["official_vectors"] = "PASS" + except FileNotFoundError as e: + print(f"[1] Official-parameter vectors : SKIP (missing fixture: {os.path.basename(str(e.filename or e))})") + status["official_vectors"] = "SKIP" + + # Anchor 2 + n2 = test_pypi_blake3() + if n2 is None: + print("[2] Official `blake3` PyPI pkg : SKIP (package not importable)") + status["pypi"] = "SKIP" + else: + print(f"[2] Official `blake3` PyPI pkg : PASS ({n2} randomised differential checks, 3 modes)") + status["pypi"] = "PASS" + + # Anchor 3 + try: + n3 = test_plonky3_differential() + print(f"[3] Plonky3 blake3-air (direct): PASS ({n3} random compressions, flags=0)") + status["plonky3"] = "PASS" + except (FileNotFoundError, ImportError) as e: + print(f"[3] Plonky3 blake3-air (direct): SKIP ({e})") + status["plonky3"] = "SKIP" + + # Internal + ni = test_internal_consistency() + print(f"[.] Internal self-consistency : PASS ({ni} checks) [not an external anchor]") + + # 6-round + differ = test_6round_derivation() + print(f"[4] 6-round variant derivation : PASS (=compress(rounds=6); differs from 7r on {differ}/2000)") + + # The banner reports what actually ran. It previously printed "VALIDATED ... + # anchored on official test vectors + official PyPI package + Plonky3" + # unconditionally, including when anchors had SKIPped — the status dict was + # written and never read. Claiming an anchor you did not run is worse than + # running none. + passed = [k for k, v in status.items() if v == "PASS"] + skipped = [k for k, v in status.items() if v == "SKIP"] + label = { + "official_vectors": "official-parameter vectors", + "pypi": "official PyPI package", + "plonky3": "Plonky3 independent compression", + } + print("=" * 74) + if not passed: + print("VALIDATION STATUS: NOT VALIDATED (no external anchor ran)") + elif skipped: + print("VALIDATION STATUS: PARTIALLY VALIDATED") + else: + print("VALIDATION STATUS: VALIDATED") + print(f" 7-round reference: anchored on {', '.join(label[k] for k in passed) or 'nothing'}.") + if skipped: + print(f" NOT anchored on : {', '.join(label[k] for k in skipped)} (skipped this run).") + print(" 6-round variant : derivative anchor (loop-bound diff) + canonical vectors below.") + print("=" * 74) + + # Emit canonical 6-round vectors. + print("\nCANONICAL 6-ROUND VARIANT VECTORS (seeds 0..9):") + vecs = canonical_6round_vectors() + out_json = os.path.join(HERE, "canonical_6round_vectors.json") + json.dump(vecs, open(out_json, "w"), indent=2) + for v in vecs: + out_hex = "".join(f"{w:08x}" for w in v["out"]) + print(f" seed={v['seed']}: t={v['t']:#018x} block_len={v['block_len']:2d} " + f"flags={v['flags']:#04x} -> out[0]={v['out'][0]:#010x} out[15]={v['out'][15]:#010x}") + print(f" (full vectors written to {os.path.basename(out_json)})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/thoughts/blake3/ground-truth/Cargo.toml b/thoughts/blake3/ground-truth/Cargo.toml new file mode 100644 index 000000000..01cc4bf46 --- /dev/null +++ b/thoughts/blake3/ground-truth/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] + +[package] +name = "gt" +version = "0.1.0" +edition = "2021" + +[dependencies] +blake3 = { version = "1.8.5", default-features = false, features = ["std", "pure"] } + +[profile.dev] +debug = false diff --git a/thoughts/blake3/ground-truth/src/bin/counter_probe.rs b/thoughts/blake3/ground-truth/src/bin/counter_probe.rs new file mode 100644 index 000000000..ac3dadff7 --- /dev/null +++ b/thoughts/blake3/ground-truth/src/bin/counter_probe.rs @@ -0,0 +1,32 @@ +// SCRATCH (audit, not committed): probe the XOF counter path of the official +// blake3 crate. For a fixed single-block input, the root output block at +// counter t is compress(key, block, t, block_len, flags|ROOT) — so seeking an +// OutputReader to byte position t*64 exercises the v[12]/v[13] counter split +// at arbitrary t, including t >= 2^32. +use blake3::Hasher; +use std::io::Write; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() +} + +fn main() { + // 64-byte single-block input, same pattern as the Python side. + let input: Vec = (0..64).map(|i| (i % 251) as u8).collect(); + let counters: Vec = vec![ + 0, 1, 2, 0xFFFF_FFFE, 0xFFFF_FFFF, 0x1_0000_0000, 0x1_0000_0001, + 0x100_0000_0000, // 2^40 + 0x8000_0000_0000, // 2^47 + ]; + let stdout = std::io::stdout(); + let mut w = std::io::BufWriter::new(stdout.lock()); + for &t in &counters { + let mut h = Hasher::new(); + h.update(&input); + let mut reader = h.finalize_xof(); + reader.set_position(t * 64); + let mut out = [0u8; 64]; + reader.fill(&mut out); + writeln!(w, "{} {}", t, hex(&out)).unwrap(); + } +} diff --git a/thoughts/blake3/ground-truth/src/main.rs b/thoughts/blake3/ground-truth/src/main.rs new file mode 100644 index 000000000..e79a10660 --- /dev/null +++ b/thoughts/blake3/ground-truth/src/main.rs @@ -0,0 +1,138 @@ +// Ground-truth BLAKE3 vector generator using the OFFICIAL blake3 crate (v1.8.5, +// pure-Rust feature, built offline from the local cargo registry). +// Emits JSON on stdout in the same shape as the upstream test_vectors.json, +// plus a randomised differential set. + +use blake3::Hasher; +use std::io::Write; + +const KEY: &[u8; 32] = b"whats the Elvish word for friend"; +const CONTEXT: &str = "BLAKE3 2019-12-27 16:29:52 test vectors context"; +const XOF_LEN: usize = 131; + +fn pattern_input(n: usize) -> Vec { + (0..n).map(|i| (i % 251) as u8).collect() +} + +fn hash_hex(input: &[u8], out_len: usize) -> String { + let mut h = Hasher::new(); + h.update(input); + let mut out = vec![0u8; out_len]; + h.finalize_xof().fill(&mut out); + hex(&out) +} + +fn keyed_hex(key: &[u8; 32], input: &[u8], out_len: usize) -> String { + let mut h = Hasher::new_keyed(key); + h.update(input); + let mut out = vec![0u8; out_len]; + h.finalize_xof().fill(&mut out); + hex(&out) +} + +fn derive_hex(ctx: &str, input: &[u8], out_len: usize) -> String { + let mut h = Hasher::new_derive_key(ctx); + h.update(input); + let mut out = vec![0u8; out_len]; + h.finalize_xof().fill(&mut out); + hex(&out) +} + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() +} + +// xorshift64* — deterministic, self-contained RNG so the Python side can +// reproduce the exact same inputs without sharing any code. +struct Rng(u64); +impl Rng { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545F4914F6CDD1D) + } + fn byte(&mut self) -> u8 { + (self.next_u64() >> 33) as u8 + } +} + +fn main() { + let lengths: Vec = vec![ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 63, 64, 65, 127, 128, 129, 1023, 1024, 1025, 2048, 2049, 3072, + 3073, 4096, 4097, 5120, 5121, 6144, 6145, 7168, 7169, 8192, 8193, 16384, 31744, 102400, + ]; + + let stdout = std::io::stdout(); + let mut w = std::io::BufWriter::new(stdout.lock()); + + writeln!(w, "{{").unwrap(); + writeln!(w, " \"key\": \"{}\",", String::from_utf8_lossy(KEY)).unwrap(); + writeln!(w, " \"context_string\": \"{}\",", CONTEXT).unwrap(); + writeln!(w, " \"cases\": [").unwrap(); + for (i, &n) in lengths.iter().enumerate() { + let inp = pattern_input(n); + writeln!(w, " {{").unwrap(); + writeln!(w, " \"input_len\": {},", n).unwrap(); + writeln!(w, " \"hash\": \"{}\",", hash_hex(&inp, XOF_LEN)).unwrap(); + writeln!(w, " \"keyed_hash\": \"{}\",", keyed_hex(KEY, &inp, XOF_LEN)).unwrap(); + writeln!(w, " \"derive_key\": \"{}\"", derive_hex(CONTEXT, &inp, XOF_LEN)).unwrap(); + writeln!(w, " }}{}", if i + 1 == lengths.len() { "" } else { "," }).unwrap(); + } + writeln!(w, " ],").unwrap(); + + // Randomised differential set. Inputs are generated from a self-contained + // xorshift64* stream that the Python side re-implements independently. + writeln!(w, " \"random\": [").unwrap(); + let rlens: Vec = vec![ + 0, 1, 2, 31, 32, 33, 63, 64, 65, 127, 128, 129, 512, 1000, 1023, 1024, 1025, 2048, 4096, + 4097, 10000, 65536, 100000, + ]; + let xofs: Vec = vec![16, 32, 64, 131, 200]; + let mut seedctr: u64 = 1; + let mut first = true; + for &n in &rlens { + for &xl in &xofs { + let seed = seedctr; + seedctr += 1; + let mut rng = Rng(seed); + let msg: Vec = (0..n).map(|_| rng.byte()).collect(); + let mut krng = Rng(seed ^ 0xDEADBEEF); + let mut key = [0u8; 32]; + for b in key.iter_mut() { + *b = krng.byte(); + } + let ctx = format!("lambda-vm oracle review ctx {}/{}", n, xl); + if !first { + writeln!(w, ",").unwrap(); + } + first = false; + write!( + w, + " {{\"seed\": {}, \"len\": {}, \"xof\": {}, \"key\": \"{}\", \"ctx\": \"{}\", \"hash\": \"{}\", \"keyed\": \"{}\", \"derive\": \"{}\"}}", + seed, + n, + xl, + hex(&key), + ctx, + hash_hex(&msg, xl), + keyed_hex(&key, &msg, xl), + derive_hex(&ctx, &msg, xl) + ) + .unwrap(); + } + } + writeln!(w, "\n ],").unwrap(); + + // A couple of well-known digests, for a human sanity check. + writeln!( + w, + " \"known\": {{\"empty\": \"{}\", \"abc\": \"{}\"}}", + hash_hex(b"", 32), + hash_hex(b"abc", 32) + ) + .unwrap(); + writeln!(w, "}}").unwrap(); +} diff --git a/thoughts/blake3/poseidon2-cost-study.md b/thoughts/blake3/poseidon2-cost-study.md new file mode 100644 index 000000000..887902f11 --- /dev/null +++ b/thoughts/blake3/poseidon2-cost-study.md @@ -0,0 +1,123 @@ +# Poseidon2 accelerator — cost study vs BLAKE3-6r and keccak (2026-08-05) + +Produced by a multi-agent study (three mining agents over the vendored +references in `others/` — Plonky3, zisk, stwo, openvm, SP1 old+new, risc0, +airbender, pil2-proofman — plus a synthesis agent applying this repo's cost +model). Model calibration: reproduces Plonky3's Goldilocks w8/SR=1 column +count exactly (180) and zisk's measured 490 cells/perm to 1.0%. + +Companion measured numbers (this branch, 32-core box, blowup 2): +keccak-f 72,672 table / 73,020 end-to-end; BLAKE3-6r 5,316 table / 7,337 +end-to-end per 2-to-1 merge; blake3 throughput 5,217 compressions/s at 2^17 +rows vs keccak 433 perms/s at 2^20 rows. + +--- + +Both calibrations land: my model reproduces Plonky3's Goldilocks w8/SR=1 figure **exactly** (180), and at zisk's degree budget it gives 495 against zisk's measured 490 — **1.0%**. That's a two-point validation of the whole cost model before applying it to our constraints. + +--- + +# Poseidon2 accelerator chip — cost study (final) + +**Headline: ≈ 651 cell-equiv table-only per 2-to-1 merge** (recommended in-place ABI; 753 under the brief's separate-output ABI). Against BLAKE3-6r's 5,316 that is **8.2× cheaper**; against keccak-f's 72,672, **112×**. End-to-end the advantage over BLAKE3 holds at roughly 5–9×, but the absolute win is small change next to what BLAKE3 already banked. + +## Calibration first — the model reproduces two independent mined numbers + +Before trusting it on our constraints, I ran the same model at other systems' degree budgets: + +| target | their budget | my model | mined | agreement | +|---|---|---:|---:|---:| +| Plonky3 Goldilocks w8, `SBOX_REGISTERS=1`, lookup-free | deg 3, ungated | `8 + core(8,REG=1)` = **180** | 180 | **exact** | +| zisk Goldilocks perm, no sbox registers, incl. memory plumbing | deg 7, ungated | `187 + 86 + 24 + 198` = **495** | 490 | **1.0%** | + +The zisk check is the valuable one: it exercises the core formula, the byte-level I/O apparatus *and* the LogUp aux rate simultaneously, and lands within 1%. It also isolates the one thing that makes our number bigger than everyone else's — the degree budget, nothing else. + +## (b) Our number, line by line + +**Width 8, truncated permutation — justified.** A digest is 4 Goldilocks elements (32 B), so a 2-to-1 merge absorbs 8 elements. Two shapes do that in *one* permutation: width 8 as a truncated permutation (`P(left‖right)[0..4]` — Plonky3's `TruncatedPermutation`, `others/Plonky3/symmetric/src/compression.rs:17`), or width 12 as a rate-8/capacity-4 sponge (Plonky2 style). I priced both on identical I/O: **width 8 = 651, width 12 = 779**. Width 8 wins by 16% and is what Plonky3/SP1 ship for merges. Parameters are forced: `RF = 8 (4+4)`, `RP = 22`, S-box `x⁷` — `others/Plonky3/goldilocks/src/poseidon2.rs:22,32,70-73`; x³ and x⁵ are not permutations since `p−1 = 2^32·3·5·17·257·65537` (`goldilocks/src/poseidon1.rs:41-44`). + +**4 committed cells per S-box — forced by μ-gating, and minimal.** Max degree 3 *including* ×μ means bodies are capped at degree 2. Chain: `a=x²`, `b=a·x=x³`, `c=b·b=x⁶`, then `post = M·(c·x)` absorbs the last multiply into the linear layer. All four constraints are degree 2 → 3 after ×μ. Four is provably minimal: from `{1}`, three degree-≤2 steps reach at most exponent 6. This is Plonky3's `SBOX_REGISTERS=3` — their width formula (`poseidon2-air/src/columns.rs:12-69`) is generic in REGISTERS, but `eval_sbox` (`air.rs:288-323`) only ships `(7,1)→deg 3`, so we are one rung past anything in the wild. + +Committing the S-box *output* (Plonky3's `post_sbox`, `air.rs:274-277`) rather than the post-linear element (SP1's `s0`) keeps the whole state at expression-degree 1 through all 22 internal rounds, so the 7 non-S-boxed elements ride free and **no boundary re-commit is needed** — SP1's choice would cost +8 cells here. + +``` +CORE (one row per permutation, fully unrolled) + full rounds 2 × 4 rounds × 8 elems × (3 registers + 1 post) = 256 + partial rounds 22 rounds × 1 elem × (3 registers + 1) = 88 + core = 344 cells + sends in the core = 0 + — field-native: no ByteAlu, no AreBytes, no lookups whatsoever + cross-check: 8 inputs + 344 = 352 = Plonky3 num_cols<8,7,3,4,22> ✓ + +CANONICITY (byte→field must be injective or the tree isn't binding: + x and x+p are distinct byte strings with the same field element) + per element: commit is_max, dinv; constrain + μ·(is_max + (H−(2³²−1))·dinv − 1) deg 3 + μ·(is_max·(H−(2³²−1))) deg 3 + μ·(is_max·L) deg 3 booleanity implied + 2 cells, 0 sends × 12 elements = 24 cells + +I/O APPARATUS (idiom copied from the shipped chip, prover/src/tables/blake3.rs:97-123 + columns and :747-1030 interactions; 2 bytes per AreBytes send, 4 IsHalfword per + dword pointer, pointer-arith carries are expression-form with no cells) + + A: 12 dwords (brief) A′: 8 dwords, in-place (SP1 ABI) + TIMESTAMP_0/1 2 2 + ADDR bytes 8 8 + PTR halfwords 48 32 + IN bytes 64 64 + OUT bytes 32 32 + OLD_OUT bytes 32 0 ← old = the input bytes + MU 1 1 + I/O columns 187 139 + + Ecall receive 1 1 + Memw register read 1 1 + Memw per dword 12 8 + IsHalfword 48 32 + AreBytes addr 4 4 + ByteAlu AND (align) 1 1 + AreBytes IN/OUT/OLD 64 48 + sends N 131 95 + +TOTAL + A : main 187+344+24 = 555 ; aux = 1.5×131 = 198 ; TOTAL 753 + A′: main 139+344+24 = 507 ; aux = 1.5× 95 = 144 ; TOTAL 651 ← recommended +``` + +Arithmetic machine-checked: `/private/tmp/claude-501/-Users-maurofab-workspace-lambda-vm/931cf0e4-cfb3-4d8a-b940-5360f4374a8b/scratchpad/pos2_cost.py`. + +**End-to-end plumbing — the weakest number here, and I won't pretend otherwise.** The two known marginals disagree about what a memory op costs: BLAKE3 is `7,337 − 5,316 = 2,021` over 23 chip Memw ops (**88/op**); keccak is `73,020 − 72,672 = 348` over 26 (25 lanes + register read, `prover/src/tables/keccak.rs:3-5`) — **13/op**. A 6.5× spread means "per Memw op" is the wrong model. The likely driver is guest-side marshalling: BLAKE3's ABI makes the guest lay out a fresh 176-byte region every call, while keccak operates in place on a resident 200-byte state (hypothesis, unverified). Poseidon2-A′ is in-place over a 96-byte region with 9 ops, i.e. structurally keccak-shaped, so I expect the low end — but I quote the full band: + +``` +A′ end-to-end = 651 + 9 ops × [13 … 88] = [768 … 1,443] central estimate ≈ 900 +A end-to-end = 753 + 13 ops × [13 … 88] = [922 … 1,897] +``` + +## (c) Comparison, per 2-to-1 merge (64 B in, 32 B out) + +| | table-only | end-to-end | vs keccak (e2e) | vs BLAKE3-6r (e2e) | +|---|---:|---:|---:|---:| +| keccak-f (measured) | 72,672 | 73,020 | 1× | 0.10× | +| BLAKE3-6r (measured) | 5,316 | 7,337 | 10.0× | 1× | +| **Poseidon2 A** (derived) | **753** | ~922–1,897 (est) | 39–79× | 3.9–8.0× | +| **Poseidon2 A′** (derived, recommended) | **651** | ~768–1,443 (est) | 51–95× | 5.1–9.6× | +| Poseidon2 B (deg-4 bodies) | 479 | ~596–1,271 (est) | 57–123× | 5.8–12.3× | +| Poseidon2 C (internal bus, no memory) | 358 | 358 | 204× | 20.5× | + +Note the shape change between the two columns: table-only, Poseidon2 looks 112× better than keccak; end-to-end that collapses toward ~50–95×, because keccak's plumbing is rounding error against its enormous table while Poseidon2's plumbing is comparable to its entire chip. + +## (d) Caveats + +1. **The chip is I/O-bound, not hash-bound.** Core 344 cells; syscall apparatus 283 cell-equiv (139 cols + 144 aux) even in the in-place variant. Every lever that removes memory crossing beats every lever inside the permutation: in-place ABI −14%, internal Merkle-parent bus −45% (358). +2. **Canonical `< p` input range checks are required and cheap.** 2 cells + 3 constraints per element, 0 sends, 24 cells for all 12. SP1 Hypercube ships exactly this check on both inputs *and* outputs (`others/hypercube-verifier/crates/core/machine/src/operations/sp1_field_word.rs:44-88`; `input_range_checkers[16]` + `hash_result_range_checkers[16]` at `syscall/precompiles/poseidon2/air.rs:66-70`) — so it isn't optional in practice. Separately: absorbing *arbitrary* byte strings rather than chip-produced digests needs 7-byte-per-element packing to stay injective, cutting sponge rate 32 B → 28 B. +3. **The degree budget is the one thing making us expensive, and it's ours alone.** Every mined design runs ungated bodies. Our ×μ factor doubles the core (344 vs 172 at deg-3 bodies) and quadruples it against zisk's deg-7 budget (344 vs 86). Good news: `logup_max_degree` already floors any table with committed pairs at 3 (`crypto/stark/src/lookup.rs:2287-2298`), so degree 3 is free. Going to 4 costs one composition part for that table alone — `composition_poly_degree_bound = trace_length·(max_degree−1)` (`lookup.rs:1078`), i.e. 3 parts instead of 2 — in exchange for −172 cells/row. That trade is plausibly a win and should be measured, not assumed. +4. **The verifier hash must switch, and that is the real bill.** All three Merkle backends are keccak (`crypto/stark/src/config.rs:10,19,23`). A Poseidon2 chip pays for nothing unless FRI/Merkle/FS move to Poseidon2 — which means a new GPU Merkle kernel (the keccak one is at `crypto/stark/src/gpu_lde.rs:861`) and a native-prover slowdown of roughly 5–10× per byte versus keccak (**order-of-magnitude, unmeasured**). BLAKE3 is the opposite trade: faster than keccak natively, so switching costs the prover nothing. This asymmetry appears nowhere in the cell count and is the single biggest difference between the two candidates. +5. **Keccak is not displaceable either way.** EVM/ethrex needs keccak256. Poseidon2 and BLAKE3-6r compete for the same internal-hash slot. +6. **Constraint-eval cost ≠ cell cost.** The 22 internal rounds carry non-S-boxed state as symbolic linear combinations — degree stays 1 (that's the point) but fan-out reaches ~30 terms by the last round, ~700 extra field mults per row. Fine, provided the IR stays a DAG. +7. **Always-on AIR tax.** `FIXED_TABLE_COUNT` +1. Per the EC regression (PR #871: +3 near-empty AIRs → +25% prove time), a real-block ABBA is mandatory regardless of how good the cell count looks. +8. **Uncertainty.** Core 344 is exact given the design and validated to 1% against zisk. I/O is exact given the shipped idiom. Table-only band: **620–700 for A′**. End-to-end is the soft number, band **768–1,443**, and it is directly measurable rather than arguable. + +## (e) Verdict + +Poseidon2 beats BLAKE3-6r here, and by a solid margin: **8.2× table-only (651 vs 5,316), 5–9× end-to-end.** The derivation is well-anchored — the same model reproduces Plonky3's Goldilocks figure exactly and zisk's to 1% — so I'd defend the number itself. What I would not defend is the conclusion that this justifies building it. Measured against keccak end-to-end, BLAKE3-6r already captures **91%** of the total addressable saving per merge (65,683 of 72,120 cell-equiv); Poseidon2 adds the remaining 9%. And Poseidon2 cannot go much lower as a syscall — roughly half its cost is the ecall/MEMW apparatus it shares with every other chip, so even a perfect permutation would only reach ~400. Meanwhile it uniquely imposes a native-prover hashing slowdown and a new GPU Merkle kernel that BLAKE3 does not, and the in-VM digests it produces are field elements crossing a byte-addressed memory, which is what the canonicity gadget and the 64 AreBytes sends are paying for. The decision should turn on one measurement nobody has taken: after the BLAKE3 switch, what share of a real recursion-verifier trace is still hashing? That is precisely the question the EC campaign skipped — a −61.9% win on 0.61% of the trace — and it is cheap to answer before committing to a chip. If Poseidon2 is pursued anyway, the leverage order is unambiguous and none of it lives in the permutation: in-place ABI (−14%), internal Merkle-parent bus (−45%), then relaxing the μ-gated degree cap (−23%). \ No newline at end of file