From 23a46692850eaf47f1163a9ac7b2f1d738edbb68 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 9 Apr 2026 17:12:49 -0300 Subject: [PATCH 1/9] Batch 24 CPU IS_BYTE range checks into 12 IS_HALF halfword checks, reducing interactions from 68 to 56 --- prover/src/tables/cpu.rs | 94 +++++++++++++----------------- prover/src/tables/trace_builder.rs | 17 ++++-- prover/src/tests/cpu_tests.rs | 7 ++- 3 files changed, 58 insertions(+), 60 deletions(-) diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index e29ae1d57..23e94ee0a 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -495,9 +495,9 @@ impl CpuOperation { let arg2 = self.compute_arg2(); let res = self.compute_res(); - let mut ops = Vec::with_capacity(27); + let mut ops = Vec::with_capacity(15); - // Register indices + // 3 IS_BYTE for register indices (single bytes, can't pair) ops.push(BitwiseOperation::single_byte( BitwiseOperationType::IsByte, self.decode.rs1, @@ -511,24 +511,18 @@ impl CpuOperation { self.decode.rd, )); - // ARG1[0..7], ARG2[0..7], RES[0..7] - for i in 0..8 { - ops.push(BitwiseOperation::single_byte( - BitwiseOperationType::IsByte, - ((arg1 >> (i * 8)) & 0xFF) as u8, - )); - } - for i in 0..8 { - ops.push(BitwiseOperation::single_byte( - BitwiseOperationType::IsByte, - ((arg2 >> (i * 8)) & 0xFF) as u8, - )); - } - for i in 0..8 { - ops.push(BitwiseOperation::single_byte( - BitwiseOperationType::IsByte, - ((res >> (i * 8)) & 0xFF) as u8, - )); + // 12 IS_HALF for ARG1/ARG2/RES byte pairs + // Adjacent bytes are batched: IS_HALF(lo + 256*hi) replaces IS_BYTE(lo) + IS_BYTE(hi) + for value in [arg1, arg2, res] { + for i in 0..4 { + let lo = ((value >> (i * 16)) & 0xFF) as u8; + let hi = ((value >> (i * 16 + 8)) & 0xFF) as u8; + ops.push(BitwiseOperation::halfword( + BitwiseOperationType::IsHalf, + lo, + hi, + )); + } } ops @@ -1949,42 +1943,18 @@ pub fn bus_interactions() -> Vec { )); // ------------------------------------------------------------------------- - // IS_BYTE interactions (byte range checks, 27 total) + // Range checks (15 total: 3 IS_BYTE + 12 IS_HALF) // CPU-CR29: IS_BYTE[rs1], CPU-CR30: IS_BYTE[rs2], CPU-CR31: IS_BYTE[rd] - // CPU-CR32.i: IS_BYTE[arg1[i]], CPU-CR33.i: IS_BYTE[arg2[i]], CPU-CR34.i: IS_BYTE[res[i]] + // CPU-CR32.i: IS_HALF[arg1[2i] + 256*arg1[2i+1]] (i=0..3) + // CPU-CR33.i: IS_HALF[arg2[2i] + 256*arg2[2i+1]] (i=0..3) + // CPU-CR34.i: IS_HALF[res[2i] + 256*res[2i+1]] (i=0..3) // ------------------------------------------------------------------------- - // Range-check all 27 byte columns: RS1, RS2, RD, ARG1[0..7], ARG2[0..7], RES[0..7]. + // RS1, RS2, RD are single-byte register indices — checked individually. + // ARG1/ARG2/RES are 8-byte little-endian values — adjacent byte pairs are + // batched into halfword checks. IS_HALF(lo + 256*hi) implies both bytes + // are in [0, 256), so one halfword check replaces two byte checks. // Every CPU row (including padding) sends with Multiplicity::One. - let byte_columns: [usize; 27] = [ - cols::RS1, - cols::RS2, - cols::RD, - cols::ARG1[0], - cols::ARG1[1], - cols::ARG1[2], - cols::ARG1[3], - cols::ARG1[4], - cols::ARG1[5], - cols::ARG1[6], - cols::ARG1[7], - cols::ARG2[0], - cols::ARG2[1], - cols::ARG2[2], - cols::ARG2[3], - cols::ARG2[4], - cols::ARG2[5], - cols::ARG2[6], - cols::ARG2[7], - cols::RES[0], - cols::RES[1], - cols::RES[2], - cols::RES[3], - cols::RES[4], - cols::RES[5], - cols::RES[6], - cols::RES[7], - ]; - for col in byte_columns { + for col in [cols::RS1, cols::RS2, cols::RD] { interactions.push(BusInteraction::sender( BusId::IsByte, Multiplicity::One, @@ -1994,6 +1964,24 @@ pub fn bus_interactions() -> Vec { }], )); } + for arr in [&cols::ARG1, &cols::ARG2, &cols::RES] { + for i in 0..4 { + interactions.push(BusInteraction::sender( + BusId::IsHalfword, + Multiplicity::One, + vec![BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: arr[2 * i], + }, + LinearTerm::Column { + coefficient: 256, + column: arr[2 * i + 1], + }, + ])], + )); + } + } // ECALL interaction (single shared bus for HALT and COMMIT) // ------------------------------------------------------------------------- diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 6cbcd0735..50accbf47 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1302,23 +1302,32 @@ fn collect_bitwise_from_branch(branch_ops: &[BranchOperation]) -> Vec Vec { if num_padding_rows == 0 { return Vec::new(); } - let mut ops = Vec::with_capacity(num_padding_rows * 27); + let mut ops = Vec::with_capacity(num_padding_rows * 15); for _ in 0..num_padding_rows { - for _ in 0..27 { + // 3 IS_BYTE for RS1, RS2, RD (all zero in padding) + for _ in 0..3 { ops.push(BitwiseOperation::single_byte( BitwiseOperationType::IsByte, 0, )); } + // 12 IS_HALF for ARG1/ARG2/RES byte pairs (all zero in padding) + for _ in 0..12 { + ops.push(BitwiseOperation::halfword( + BitwiseOperationType::IsHalf, + 0, + 0, + )); + } } ops } diff --git a/prover/src/tests/cpu_tests.rs b/prover/src/tests/cpu_tests.rs index d2c240293..d1a6d5b36 100644 --- a/prover/src/tests/cpu_tests.rs +++ b/prover/src/tests/cpu_tests.rs @@ -329,9 +329,10 @@ fn test_bus_interactions_count() { // - 1 SHIFT (shift operations) // - 1 BRANCH (branch/jump target calculation) // - 1 ECALL (single shared bus for HALT and COMMIT, mult = ECALL) - // - 27 IS_BYTE (byte range checks: RS1, RS2, RD, ARG1[0..7], ARG2[0..7], RES[0..7]) - // Total: 8 + 8 + 8 + 2 + 1 + 1 + 1 + 1 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 27 = 68 - assert_eq!(interactions.len(), 68); + // - 3 IS_BYTE (register indices: RS1, RS2, RD) + // - 12 IS_HALF (ARG1/ARG2/RES byte pairs: 4 halfwords × 3 arrays) + // Total: 8 + 8 + 8 + 2 + 1 + 1 + 1 + 1 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 3 + 12 = 56 + assert_eq!(interactions.len(), 56); } #[test] From 443e8bf27cdbe9807e7408c23733948f8d08d600 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 9 Apr 2026 18:37:26 -0300 Subject: [PATCH 2/9] Fix soundness: replace IS_HALF with IsBytePair that sends two separate bus values for individual byte verification --- prover/src/tables/bitwise.rs | 26 +++++++++++++++-- prover/src/tables/cpu.rs | 34 +++++++++++----------- prover/src/tables/trace_builder.rs | 8 +++--- prover/src/tables/types.rs | 45 +++++++++++++++++------------- prover/src/test_utils.rs | 6 ++-- prover/src/tests/bitwise_tests.rs | 4 +-- 6 files changed, 77 insertions(+), 46 deletions(-) diff --git a/prover/src/tables/bitwise.rs b/prover/src/tables/bitwise.rs index 4b1e99d09..ea00669c8 100644 --- a/prover/src/tables/bitwise.rs +++ b/prover/src/tables/bitwise.rs @@ -92,9 +92,11 @@ pub mod cols { pub const MU_IS_B20: usize = 19; /// Multiplicity for HWSL lookups pub const MU_HWSL: usize = 20; + /// Multiplicity for IS_BYTE_PAIR lookups (two bytes checked individually) + pub const MU_IS_BYTE_PAIR: usize = 21; /// Total number of columns - pub const NUM_COLUMNS: usize = 21; + pub const NUM_COLUMNS: usize = 22; } /// Number of rows in the BITWISE table: 256 * 256 * 16 = 2^20 @@ -382,6 +384,7 @@ pub fn update_multiplicities( BitwiseOperationType::IsHalf => cols::MU_IS_HALF, BitwiseOperationType::IsB20 => cols::MU_IS_B20, BitwiseOperationType::Hwsl => cols::MU_HWSL, + BitwiseOperationType::IsBytePair => cols::MU_IS_BYTE_PAIR, }; // Increment multiplicity @@ -418,7 +421,7 @@ pub(crate) fn trim_zero_rows( .filter(|&row| { let row_data = trace.main_table.get_row(row); // Check all multiplicity columns (indices 11-21) - (cols::MU_AND..=cols::MU_HWSL).any(|col| row_data[col] != FE::zero()) + (cols::MU_AND..=cols::MU_IS_BYTE_PAIR).any(|col| row_data[col] != FE::zero()) }) .collect(); @@ -459,6 +462,8 @@ pub enum BitwiseOperationType { IsHalf, IsB20, Hwsl, + /// Two bytes checked individually via separate bus values. + IsBytePair, } /// A lookup request to the BITWISE precomputed table. @@ -710,6 +715,23 @@ pub fn bus_interactions() -> Vec { }, ])], ), + // IS_BYTE_PAIR[X, Y] - range check two individual bytes via separate bus values. + // Unlike IS_HALF (which sends X+256*Y as one element), this sends X and Y + // as two separate fingerprint elements, so LogUp forces each to match individually. + BusInteraction::receiver( + BusId::IsBytePair, + Multiplicity::Column(cols::MU_IS_BYTE_PAIR), + vec![ + BusValue::Packed { + start_column: cols::X, + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::Y, + packing: Packing::Direct, + }, + ], + ), // HWSL[X + 256*Y, Z] -> [SLL, SLLC] BusInteraction::receiver( BusId::Hwsl, diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 23e94ee0a..c23ceb85c 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -511,14 +511,15 @@ impl CpuOperation { self.decode.rd, )); - // 12 IS_HALF for ARG1/ARG2/RES byte pairs - // Adjacent bytes are batched: IS_HALF(lo + 256*hi) replaces IS_BYTE(lo) + IS_BYTE(hi) + // 12 IS_BYTE_PAIR for ARG1/ARG2/RES byte pairs + // Each pair sends [lo, hi] as two separate bus values, so the LogUp + // fingerprint forces each byte to match individually against BITWISE X, Y. for value in [arg1, arg2, res] { for i in 0..4 { let lo = ((value >> (i * 16)) & 0xFF) as u8; let hi = ((value >> (i * 16 + 8)) & 0xFF) as u8; - ops.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::IsBytePair, lo, hi, )); @@ -533,7 +534,7 @@ impl CpuOperation { use super::bitwise::{BitwiseOperation, BitwiseOperationType}; let mut lookups = Vec::new(); - // Byte range checks: 27 IS_BYTE + // Range checks: 3 IS_BYTE + 12 IS_BYTE_PAIR = 15 ops lookups.extend(self.collect_byte_check_ops()); // MSB16 lookups for sign bit extraction (when word_instr=1) @@ -1951,8 +1952,9 @@ pub fn bus_interactions() -> Vec { // ------------------------------------------------------------------------- // RS1, RS2, RD are single-byte register indices — checked individually. // ARG1/ARG2/RES are 8-byte little-endian values — adjacent byte pairs are - // batched into halfword checks. IS_HALF(lo + 256*hi) implies both bytes - // are in [0, 256), so one halfword check replaces two byte checks. + // batched into IS_BYTE_PAIR checks. Each pair sends two separate bus values + // [lo, hi], so the LogUp fingerprint forces each byte to match individually + // against the BITWISE table's X ∈ [0,255] and Y ∈ [0,255]. // Every CPU row (including padding) sends with Multiplicity::One. for col in [cols::RS1, cols::RS2, cols::RD] { interactions.push(BusInteraction::sender( @@ -1967,18 +1969,18 @@ pub fn bus_interactions() -> Vec { for arr in [&cols::ARG1, &cols::ARG2, &cols::RES] { for i in 0..4 { interactions.push(BusInteraction::sender( - BusId::IsHalfword, + BusId::IsBytePair, Multiplicity::One, - vec![BusValue::linear(vec![ - LinearTerm::Column { - coefficient: 1, - column: arr[2 * i], + vec![ + BusValue::Packed { + start_column: arr[2 * i], + packing: Packing::Direct, }, - LinearTerm::Column { - coefficient: 256, - column: arr[2 * i + 1], + BusValue::Packed { + start_column: arr[2 * i + 1], + packing: Packing::Direct, }, - ])], + ], )); } } diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 50accbf47..c50c80dbd 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1305,7 +1305,7 @@ fn collect_bitwise_from_branch(branch_ops: &[BranchOperation]) -> Vec Vec { if num_padding_rows == 0 { return Vec::new(); @@ -1320,10 +1320,10 @@ fn collect_byte_check_ops_for_padding(num_padding_rows: usize) -> Vec "IsByte", BusId::IsHalfword => "IsHalfword", + BusId::IsBytePair => "IsBytePair", BusId::IsB20 => "IsB20", BusId::AndByte => "AndByte", BusId::OrByte => "OrByte", @@ -148,26 +152,27 @@ impl TryFrom for BusId { match value { 0 => Ok(BusId::IsByte), 1 => Ok(BusId::IsHalfword), - 2 => Ok(BusId::IsB20), - 3 => Ok(BusId::AndByte), - 4 => Ok(BusId::OrByte), - 5 => Ok(BusId::XorByte), - 6 => Ok(BusId::Msb8), - 7 => Ok(BusId::Msb16), - 8 => Ok(BusId::Zero), - 9 => Ok(BusId::Hwsl), - 10 => Ok(BusId::Lt), - 11 => Ok(BusId::Mul), - 12 => Ok(BusId::Dvrm), - 13 => Ok(BusId::Shift), - 14 => Ok(BusId::Memw), - 15 => Ok(BusId::Load), - 16 => Ok(BusId::Memory), - 17 => Ok(BusId::Branch), - 18 => Ok(BusId::Decode), - 19 => Ok(BusId::Ecall), - 20 => Ok(BusId::CommitNextByte), - 21 => Ok(BusId::Commit), + 2 => Ok(BusId::IsBytePair), + 3 => Ok(BusId::IsB20), + 4 => Ok(BusId::AndByte), + 5 => Ok(BusId::OrByte), + 6 => Ok(BusId::XorByte), + 7 => Ok(BusId::Msb8), + 8 => Ok(BusId::Msb16), + 9 => Ok(BusId::Zero), + 10 => Ok(BusId::Hwsl), + 11 => Ok(BusId::Lt), + 12 => Ok(BusId::Mul), + 13 => Ok(BusId::Dvrm), + 14 => Ok(BusId::Shift), + 15 => Ok(BusId::Memw), + 16 => Ok(BusId::Load), + 17 => Ok(BusId::Memory), + 18 => Ok(BusId::Branch), + 19 => Ok(BusId::Decode), + 20 => Ok(BusId::Ecall), + 21 => Ok(BusId::CommitNextByte), + 22 => Ok(BusId::Commit), other => Err(other), } } diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index 155af86cb..2a8151206 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -374,7 +374,7 @@ pub fn generate_minimal_bitwise_trace(ops: &[BitwiseOperation]) -> TraceTable = HashMap::new(); + let mut row_data: HashMap<(u8, u8, u8), [u64; 11]> = HashMap::new(); for op in ops { let key = (op.x, op.y, op.z); @@ -389,8 +389,9 @@ pub fn generate_minimal_bitwise_trace(ops: &[BitwiseOperation]) -> TraceTable 7, BitwiseOperationType::IsB20 => 8, BitwiseOperationType::Hwsl => 9, + BitwiseOperationType::IsBytePair => 10, }; - row_data.entry(key).or_insert([0; 10])[mu_idx] += 1; + row_data.entry(key).or_insert([0; 11])[mu_idx] += 1; } // Need at least 4 rows for FRI, pad to power of 2 @@ -448,6 +449,7 @@ pub fn generate_minimal_bitwise_trace(ops: &[BitwiseOperation]) -> TraceTable Date: Thu, 9 Apr 2026 19:03:58 -0300 Subject: [PATCH 3/9] update comments --- prover/src/tables/cpu.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index c23ceb85c..10188f010 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -36,7 +36,8 @@ //! //! ### Senders (CPU sends to other tables) //! - DECODE: instruction fetch -//! - IS_BYTE: range checks for rs1, rs2, rd, arg1[i], arg2[i], res[i] +//! - IS_BYTE: range checks for rs1, rs2, rd +//! - IS_BYTE_PAIR: range checks for arg1/arg2/res byte pairs (×12, two bytes per interaction) //! - IS_BIT: range checks for flags (via templates) //! - ADD: for ADD, LOAD, JALR operations //! - STORE ADD: for STORE (res = arg1 + imm, separate from main ADD) @@ -1944,11 +1945,11 @@ pub fn bus_interactions() -> Vec { )); // ------------------------------------------------------------------------- - // Range checks (15 total: 3 IS_BYTE + 12 IS_HALF) + // Range checks (15 total: 3 IS_BYTE + 12 IS_BYTE_PAIR) // CPU-CR29: IS_BYTE[rs1], CPU-CR30: IS_BYTE[rs2], CPU-CR31: IS_BYTE[rd] - // CPU-CR32.i: IS_HALF[arg1[2i] + 256*arg1[2i+1]] (i=0..3) - // CPU-CR33.i: IS_HALF[arg2[2i] + 256*arg2[2i+1]] (i=0..3) - // CPU-CR34.i: IS_HALF[res[2i] + 256*res[2i+1]] (i=0..3) + // CPU-CR32.i: IS_BYTE_PAIR[arg1[2i], arg1[2i+1]] (i=0..3) + // CPU-CR33.i: IS_BYTE_PAIR[arg2[2i], arg2[2i+1]] (i=0..3) + // CPU-CR34.i: IS_BYTE_PAIR[res[2i], res[2i+1]] (i=0..3) // ------------------------------------------------------------------------- // RS1, RS2, RD are single-byte register indices — checked individually. // ARG1/ARG2/RES are 8-byte little-endian values — adjacent byte pairs are From 24ab122ea843da55db77ab6b45c5abad33d6e4fc Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 13 Apr 2026 14:37:12 -0300 Subject: [PATCH 4/9] update comments --- prover/src/tables/cpu.rs | 7 ++++--- prover/src/tests/cpu_tests.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 10188f010..1f7704287 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -485,10 +485,11 @@ impl CpuOperation { } } - /// Collects IS_BYTE lookups for CPU byte range checks. + /// Collects CPU range-check lookups for register indices and byte pairs. /// - /// The CPU has 27 byte columns that must be range-checked: RS1, RS2, RD, - /// ARG1[0..7], ARG2[0..7], RES[0..7]. Each generates one IS_BYTE lookup. + /// The CPU sends: + /// - 3 IS_BYTE lookups for RS1, RS2, RD + /// - 12 IS_BYTE_PAIR lookups for adjacent bytes in ARG1, ARG2, and RES pub fn collect_byte_check_ops(&self) -> Vec { use super::bitwise::{BitwiseOperation, BitwiseOperationType}; diff --git a/prover/src/tests/cpu_tests.rs b/prover/src/tests/cpu_tests.rs index d1a6d5b36..feffd0e97 100644 --- a/prover/src/tests/cpu_tests.rs +++ b/prover/src/tests/cpu_tests.rs @@ -330,7 +330,7 @@ fn test_bus_interactions_count() { // - 1 BRANCH (branch/jump target calculation) // - 1 ECALL (single shared bus for HALT and COMMIT, mult = ECALL) // - 3 IS_BYTE (register indices: RS1, RS2, RD) - // - 12 IS_HALF (ARG1/ARG2/RES byte pairs: 4 halfwords × 3 arrays) + // - 12 IS_BYTE_PAIR (ARG1/ARG2/RES byte pairs: 4 pairs × 3 arrays) // Total: 8 + 8 + 8 + 2 + 1 + 1 + 1 + 1 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 3 + 12 = 56 assert_eq!(interactions.len(), 56); } From 009be72d43103880c35c2344cde1480622f2d8d8 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 16 Apr 2026 17:16:41 -0300 Subject: [PATCH 5/9] Unify IS_BYTE to check two bytes --- prover/src/tables/bitwise.rs | 56 +++++++++++----------------- prover/src/tables/branch.rs | 13 ++++--- prover/src/tables/cpu.rs | 44 ++++++++++++---------- prover/src/tables/page.rs | 30 +++++++++------ prover/src/tables/trace_builder.rs | 18 ++++----- prover/src/tables/types.rs | 10 ++--- prover/src/test_utils.rs | 6 +-- prover/src/tests/bitwise_tests.rs | 4 +- prover/src/tests/cpu_tests.rs | 4 +- prover/src/tests/prove_elfs_tests.rs | 4 +- 10 files changed, 91 insertions(+), 98 deletions(-) diff --git a/prover/src/tables/bitwise.rs b/prover/src/tables/bitwise.rs index ea00669c8..455f696f2 100644 --- a/prover/src/tables/bitwise.rs +++ b/prover/src/tables/bitwise.rs @@ -1,9 +1,9 @@ //! BITWISE precomputed lookup table. //! -//! This table provides 11 different lookup types used by other tables: +//! This table provides 10 different lookup types used by other tables: //! //! ## Range Checks -//! - `IS_BYTE[X]` - X is a valid byte [0, 256) +//! - `IS_BYTE[X, Y]` - X and Y are valid bytes [0, 256) //! - `IS_HALF[X]` - X is a valid halfword [0, 2^16) //! - `IS_B20[X]` - X is a valid 20-bit value [0, 2^20) //! @@ -84,7 +84,8 @@ pub mod cols { pub const MU_MSB16: usize = 15; /// Multiplicity for ZERO lookups pub const MU_ZERO: usize = 16; - /// Multiplicity for IS_BYTE lookups + /// Multiplicity for IS_BYTE lookups. Each lookup checks X and Y; pass Y=0 + /// for a single-byte range check. pub const MU_IS_BYTE: usize = 17; /// Multiplicity for IS_HALF lookups pub const MU_IS_HALF: usize = 18; @@ -92,11 +93,8 @@ pub mod cols { pub const MU_IS_B20: usize = 19; /// Multiplicity for HWSL lookups pub const MU_HWSL: usize = 20; - /// Multiplicity for IS_BYTE_PAIR lookups (two bytes checked individually) - pub const MU_IS_BYTE_PAIR: usize = 21; - /// Total number of columns - pub const NUM_COLUMNS: usize = 22; + pub const NUM_COLUMNS: usize = 21; } /// Number of rows in the BITWISE table: 256 * 256 * 16 = 2^20 @@ -384,7 +382,6 @@ pub fn update_multiplicities( BitwiseOperationType::IsHalf => cols::MU_IS_HALF, BitwiseOperationType::IsB20 => cols::MU_IS_B20, BitwiseOperationType::Hwsl => cols::MU_HWSL, - BitwiseOperationType::IsBytePair => cols::MU_IS_BYTE_PAIR, }; // Increment multiplicity @@ -420,8 +417,8 @@ pub(crate) fn trim_zero_rows( let kept_rows: Vec = (0..num_rows) .filter(|&row| { let row_data = trace.main_table.get_row(row); - // Check all multiplicity columns (indices 11-21) - (cols::MU_AND..=cols::MU_IS_BYTE_PAIR).any(|col| row_data[col] != FE::zero()) + // Check all multiplicity columns (indices 11-20) + (cols::MU_AND..=cols::MU_HWSL).any(|col| row_data[col] != FE::zero()) }) .collect(); @@ -462,8 +459,6 @@ pub enum BitwiseOperationType { IsHalf, IsB20, Hwsl, - /// Two bytes checked individually via separate bus values. - IsBytePair, } /// A lookup request to the BITWISE precomputed table. @@ -481,7 +476,8 @@ pub enum BitwiseOperationType { /// - AND/OR/XOR: `x OP y` /// - MSB8: MSB of `x` /// - MSB16: MSB of halfword `x + y * 256` -/// - IS_BYTE/IS_HALF: Range check on `x + y * 256` +/// - IS_BYTE: Range check both `x` and `y`; use `y = 0` for a single byte +/// - IS_HALF: Range check on `x + y * 256` /// - HWSL: Shift `x + y * 256` by `z` bits, returning [SLL, SLLC] as a pair #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct BitwiseOperation { @@ -672,14 +668,21 @@ pub fn bus_interactions() -> Vec { }, ], ), - // IS_BYTE[X] - range check, no output + // IS_BYTE[X, Y] - range check two byte values, no output. + // Single-byte checks send the second argument as 0. BusInteraction::receiver( BusId::IsByte, Multiplicity::Column(cols::MU_IS_BYTE), - vec![BusValue::Packed { - start_column: cols::X, - packing: Packing::Direct, - }], + vec![ + BusValue::Packed { + start_column: cols::X, + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::Y, + packing: Packing::Direct, + }, + ], ), // IS_HALF[X + 256*Y] - range check for halfword BusInteraction::receiver( @@ -715,23 +718,6 @@ pub fn bus_interactions() -> Vec { }, ])], ), - // IS_BYTE_PAIR[X, Y] - range check two individual bytes via separate bus values. - // Unlike IS_HALF (which sends X+256*Y as one element), this sends X and Y - // as two separate fingerprint elements, so LogUp forces each to match individually. - BusInteraction::receiver( - BusId::IsBytePair, - Multiplicity::Column(cols::MU_IS_BYTE_PAIR), - vec![ - BusValue::Packed { - start_column: cols::X, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::Y, - packing: Packing::Direct, - }, - ], - ), // HWSL[X + 256*Y, Z] -> [SLL, SLLC] BusInteraction::receiver( BusId::Hwsl, diff --git a/prover/src/tables/branch.rs b/prover/src/tables/branch.rs index d4ae06cff..2d42e31e5 100644 --- a/prover/src/tables/branch.rs +++ b/prover/src/tables/branch.rs @@ -236,14 +236,17 @@ pub fn generate_branch_trace( /// - **Receives** BRANCH lookups from CPU table pub fn bus_interactions() -> Vec { vec![ - // IS_BYTE[next_pc_low[1]] - range check bits 8-15 + // IS_BYTE[next_pc_low[1], 0] - range check bits 8-15 BusInteraction::sender( BusId::IsByte, Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::NEXT_PC_LOW_1, - packing: Packing::Direct, - }], + vec![ + BusValue::Packed { + start_column: cols::NEXT_PC_LOW_1, + packing: Packing::Direct, + }, + BusValue::constant(0), + ], ), // AND_BYTE[next_pc_low[0]; unmasked_low_byte, 254] // Verifies: next_pc_low[0] = unmasked_low_byte & 0xFE diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 1f7704287..5e35f7faf 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -36,8 +36,7 @@ //! //! ### Senders (CPU sends to other tables) //! - DECODE: instruction fetch -//! - IS_BYTE: range checks for rs1, rs2, rd -//! - IS_BYTE_PAIR: range checks for arg1/arg2/res byte pairs (×12, two bytes per interaction) +//! - IS_BYTE: range checks for rs1, rs2, rd, and arg1/arg2/res byte pairs //! - IS_BIT: range checks for flags (via templates) //! - ADD: for ADD, LOAD, JALR operations //! - STORE ADD: for STORE (res = arg1 + imm, separate from main ADD) @@ -488,8 +487,8 @@ impl CpuOperation { /// Collects CPU range-check lookups for register indices and byte pairs. /// /// The CPU sends: - /// - 3 IS_BYTE lookups for RS1, RS2, RD - /// - 12 IS_BYTE_PAIR lookups for adjacent bytes in ARG1, ARG2, and RES + /// - 3 IS_BYTE lookups for RS1, RS2, RD, encoded as (byte, 0) + /// - 12 IS_BYTE lookups for adjacent byte pairs in ARG1, ARG2, and RES pub fn collect_byte_check_ops(&self) -> Vec { use super::bitwise::{BitwiseOperation, BitwiseOperationType}; @@ -513,7 +512,7 @@ impl CpuOperation { self.decode.rd, )); - // 12 IS_BYTE_PAIR for ARG1/ARG2/RES byte pairs + // 12 IS_BYTE lookups for ARG1/ARG2/RES byte pairs // Each pair sends [lo, hi] as two separate bus values, so the LogUp // fingerprint forces each byte to match individually against BITWISE X, Y. for value in [arg1, arg2, res] { @@ -521,7 +520,7 @@ impl CpuOperation { let lo = ((value >> (i * 16)) & 0xFF) as u8; let hi = ((value >> (i * 16 + 8)) & 0xFF) as u8; ops.push(BitwiseOperation::byte_op( - BitwiseOperationType::IsBytePair, + BitwiseOperationType::IsByte, lo, hi, )); @@ -536,7 +535,8 @@ impl CpuOperation { use super::bitwise::{BitwiseOperation, BitwiseOperationType}; let mut lookups = Vec::new(); - // Range checks: 3 IS_BYTE + 12 IS_BYTE_PAIR = 15 ops + // Range checks: 15 IS_BYTE ops; single-byte checks use the second + // argument set to 0. lookups.extend(self.collect_byte_check_ops()); // MSB16 lookups for sign bit extraction (when word_instr=1) @@ -1946,32 +1946,36 @@ pub fn bus_interactions() -> Vec { )); // ------------------------------------------------------------------------- - // Range checks (15 total: 3 IS_BYTE + 12 IS_BYTE_PAIR) - // CPU-CR29: IS_BYTE[rs1], CPU-CR30: IS_BYTE[rs2], CPU-CR31: IS_BYTE[rd] - // CPU-CR32.i: IS_BYTE_PAIR[arg1[2i], arg1[2i+1]] (i=0..3) - // CPU-CR33.i: IS_BYTE_PAIR[arg2[2i], arg2[2i+1]] (i=0..3) - // CPU-CR34.i: IS_BYTE_PAIR[res[2i], res[2i+1]] (i=0..3) + // Range checks (15 total): + // CPU-CR29: IS_BYTE[rs1, 0], CPU-CR30: IS_BYTE[rs2, 0], CPU-CR31: IS_BYTE[rd, 0] + // CPU-CR32.i: IS_BYTE[arg1[2i], arg1[2i+1]] (i=0..3) + // CPU-CR33.i: IS_BYTE[arg2[2i], arg2[2i+1]] (i=0..3) + // CPU-CR34.i: IS_BYTE[res[2i], res[2i+1]] (i=0..3) // ------------------------------------------------------------------------- - // RS1, RS2, RD are single-byte register indices — checked individually. + // RS1, RS2, RD are single-byte register indices checked with a zero second + // argument. // ARG1/ARG2/RES are 8-byte little-endian values — adjacent byte pairs are - // batched into IS_BYTE_PAIR checks. Each pair sends two separate bus values + // batched into IS_BYTE checks. Each pair sends two separate bus values // [lo, hi], so the LogUp fingerprint forces each byte to match individually - // against the BITWISE table's X ∈ [0,255] and Y ∈ [0,255]. + // against the BITWISE table's X in [0,255] and Y in [0,255]. // Every CPU row (including padding) sends with Multiplicity::One. for col in [cols::RS1, cols::RS2, cols::RD] { interactions.push(BusInteraction::sender( BusId::IsByte, Multiplicity::One, - vec![BusValue::Packed { - start_column: col, - packing: Packing::Direct, - }], + vec![ + BusValue::Packed { + start_column: col, + packing: Packing::Direct, + }, + BusValue::constant(0), + ], )); } for arr in [&cols::ARG1, &cols::ARG2, &cols::RES] { for i in 0..4 { interactions.push(BusInteraction::sender( - BusId::IsBytePair, + BusId::IsByte, Multiplicity::One, vec![ BusValue::Packed { diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index 8d3c9c6b9..05a0c8913 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -297,8 +297,8 @@ pub fn precomputed_commitment_cached(config: &PageConfig, options: &ProofOptions /// /// ## Bus Interactions /// -/// - PAGE-C1: IS_BYTE[init] - sender, multiplicity 1 (range check) -/// - PAGE-C2: IS_BYTE[fini] - sender, multiplicity 1 (range check) +/// - PAGE-C1: IS_BYTE[init, 0] - sender, multiplicity 1 (range check) +/// - PAGE-C2: IS_BYTE[fini, 0] - sender, multiplicity 1 (range check) /// - PAGE-C3: memory[0, address, 0, init] - receiver, multiplicity -1 /// - PAGE-C4: memory[0, address, timestamp, fini] - sender, multiplicity 1 /// @@ -322,23 +322,29 @@ pub fn bus_interactions(page_base: u64) -> Vec { let address_hi = BusValue::constant(page_base_hi); vec![ - // PAGE-C1: IS_BYTE[init] - range check initial value + // PAGE-C1: IS_BYTE[init, 0] - range check initial value BusInteraction::sender( BusId::IsByte, Multiplicity::One, - vec![BusValue::Packed { - start_column: cols::INIT, - packing: Packing::Direct, - }], + vec![ + BusValue::Packed { + start_column: cols::INIT, + packing: Packing::Direct, + }, + BusValue::constant(0), + ], ), - // PAGE-C2: IS_BYTE[fini] - range check final value + // PAGE-C2: IS_BYTE[fini, 0] - range check final value BusInteraction::sender( BusId::IsByte, Multiplicity::One, - vec![BusValue::Packed { - start_column: cols::FINI, - packing: Packing::Direct, - }], + vec![ + BusValue::Packed { + start_column: cols::FINI, + packing: Packing::Direct, + }, + BusValue::constant(0), + ], ), // PAGE-C3: memory[0, address, 0, init] - receive initial token BusInteraction::receiver( diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index c50c80dbd..cff527fce 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1240,7 +1240,7 @@ fn collect_bitwise_from_dvrm(dvrm_ops: &[(DvrmOperation, bool)]) -> Vec Vec> 48) & 0xFFFF) as u16; let unmasked_low_byte = (next_pc_unmasked & 0xFF) as u8; - // IS_BYTE[next_pc_low[1]] - range check for byte value + // IS_BYTE[next_pc_low[1], 0] - range check for byte value bitwise_ops.push(BitwiseOperation::single_byte( BitwiseOperationType::IsByte, next_pc_low_1, @@ -1305,7 +1305,7 @@ fn collect_bitwise_from_branch(branch_ops: &[BranchOperation]) -> Vec Vec { if num_padding_rows == 0 { return Vec::new(); @@ -1320,10 +1320,10 @@ fn collect_byte_check_ops_for_padding(num_padding_rows: usize) -> Vec Vec Vec { @@ -1392,13 +1392,13 @@ fn collect_bitwise_from_page(elf: &Elf, memory_state: &MemoryState) -> Vec "IsByte", BusId::IsHalfword => "IsHalfword", - BusId::IsBytePair => "IsBytePair", BusId::IsB20 => "IsB20", BusId::AndByte => "AndByte", BusId::OrByte => "OrByte", @@ -152,7 +149,6 @@ impl TryFrom for BusId { match value { 0 => Ok(BusId::IsByte), 1 => Ok(BusId::IsHalfword), - 2 => Ok(BusId::IsBytePair), 3 => Ok(BusId::IsB20), 4 => Ok(BusId::AndByte), 5 => Ok(BusId::OrByte), diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index 2a8151206..155af86cb 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -374,7 +374,7 @@ pub fn generate_minimal_bitwise_trace(ops: &[BitwiseOperation]) -> TraceTable = HashMap::new(); + let mut row_data: HashMap<(u8, u8, u8), [u64; 10]> = HashMap::new(); for op in ops { let key = (op.x, op.y, op.z); @@ -389,9 +389,8 @@ pub fn generate_minimal_bitwise_trace(ops: &[BitwiseOperation]) -> TraceTable 7, BitwiseOperationType::IsB20 => 8, BitwiseOperationType::Hwsl => 9, - BitwiseOperationType::IsBytePair => 10, }; - row_data.entry(key).or_insert([0; 11])[mu_idx] += 1; + row_data.entry(key).or_insert([0; 10])[mu_idx] += 1; } // Need at least 4 rows for FRI, pad to power of 2 @@ -449,7 +448,6 @@ pub fn generate_minimal_bitwise_trace(ops: &[BitwiseOperation]) -> TraceTable Date: Thu, 16 Apr 2026 18:41:32 -0300 Subject: [PATCH 6/9] fix --- prover/src/tables/cpu.rs | 67 +++++++++++++++++------------- prover/src/tables/trace_builder.rs | 23 ++++++---- prover/src/tests/cpu_tests.rs | 7 ++-- 3 files changed, 56 insertions(+), 41 deletions(-) diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 5e35f7faf..055742101 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -487,7 +487,8 @@ impl CpuOperation { /// Collects CPU range-check lookups for register indices and byte pairs. /// /// The CPU sends: - /// - 3 IS_BYTE lookups for RS1, RS2, RD, encoded as (byte, 0) + /// - 1 IS_BYTE lookup for (RS1, RS2) batched as a pair + /// - 1 IS_BYTE lookup for RD encoded as (RD, 0) /// - 12 IS_BYTE lookups for adjacent byte pairs in ARG1, ARG2, and RES pub fn collect_byte_check_ops(&self) -> Vec { use super::bitwise::{BitwiseOperation, BitwiseOperationType}; @@ -496,15 +497,12 @@ impl CpuOperation { let arg2 = self.compute_arg2(); let res = self.compute_res(); - let mut ops = Vec::with_capacity(15); + let mut ops = Vec::with_capacity(14); - // 3 IS_BYTE for register indices (single bytes, can't pair) - ops.push(BitwiseOperation::single_byte( + // Batch RS1+RS2 as a pair; RD stays single with Y=0. + ops.push(BitwiseOperation::byte_op( BitwiseOperationType::IsByte, self.decode.rs1, - )); - ops.push(BitwiseOperation::single_byte( - BitwiseOperationType::IsByte, self.decode.rs2, )); ops.push(BitwiseOperation::single_byte( @@ -535,8 +533,8 @@ impl CpuOperation { use super::bitwise::{BitwiseOperation, BitwiseOperationType}; let mut lookups = Vec::new(); - // Range checks: 15 IS_BYTE ops; single-byte checks use the second - // argument set to 0. + // Range checks: 14 IS_BYTE ops (RS1+RS2 paired, RD single with Y=0, + // plus 12 ARG1/ARG2/RES byte pairs). lookups.extend(self.collect_byte_check_ops()); // MSB16 lookups for sign bit extraction (when word_instr=1) @@ -1946,32 +1944,43 @@ pub fn bus_interactions() -> Vec { )); // ------------------------------------------------------------------------- - // Range checks (15 total): - // CPU-CR29: IS_BYTE[rs1, 0], CPU-CR30: IS_BYTE[rs2, 0], CPU-CR31: IS_BYTE[rd, 0] - // CPU-CR32.i: IS_BYTE[arg1[2i], arg1[2i+1]] (i=0..3) - // CPU-CR33.i: IS_BYTE[arg2[2i], arg2[2i+1]] (i=0..3) - // CPU-CR34.i: IS_BYTE[res[2i], res[2i+1]] (i=0..3) + // Range checks (14 total): + // CPU-CR29: IS_BYTE[rs1, rs2], CPU-CR30: IS_BYTE[rd, 0] + // CPU-CR31.i: IS_BYTE[arg1[2i], arg1[2i+1]] (i=0..3) + // CPU-CR32.i: IS_BYTE[arg2[2i], arg2[2i+1]] (i=0..3) + // CPU-CR33.i: IS_BYTE[res[2i], res[2i+1]] (i=0..3) // ------------------------------------------------------------------------- - // RS1, RS2, RD are single-byte register indices checked with a zero second - // argument. + // RS1 and RS2 share one IS_BYTE check; RD uses 0 as the second argument. // ARG1/ARG2/RES are 8-byte little-endian values — adjacent byte pairs are // batched into IS_BYTE checks. Each pair sends two separate bus values // [lo, hi], so the LogUp fingerprint forces each byte to match individually // against the BITWISE table's X in [0,255] and Y in [0,255]. // Every CPU row (including padding) sends with Multiplicity::One. - for col in [cols::RS1, cols::RS2, cols::RD] { - interactions.push(BusInteraction::sender( - BusId::IsByte, - Multiplicity::One, - vec![ - BusValue::Packed { - start_column: col, - packing: Packing::Direct, - }, - BusValue::constant(0), - ], - )); - } + interactions.push(BusInteraction::sender( + BusId::IsByte, + Multiplicity::One, + vec![ + BusValue::Packed { + start_column: cols::RS1, + packing: Packing::Direct, + }, + BusValue::Packed { + start_column: cols::RS2, + packing: Packing::Direct, + }, + ], + )); + interactions.push(BusInteraction::sender( + BusId::IsByte, + Multiplicity::One, + vec![ + BusValue::Packed { + start_column: cols::RD, + packing: Packing::Direct, + }, + BusValue::constant(0), + ], + )); for arr in [&cols::ARG1, &cols::ARG2, &cols::RES] { for i in 0..4 { interactions.push(BusInteraction::sender( diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index cff527fce..5aec77a15 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1305,21 +1305,26 @@ fn collect_bitwise_from_branch(branch_ops: &[BranchOperation]) -> Vec Vec { if num_padding_rows == 0 { return Vec::new(); } - let mut ops = Vec::with_capacity(num_padding_rows * 15); + let mut ops = Vec::with_capacity(num_padding_rows * 14); for _ in 0..num_padding_rows { - // 3 IS_BYTE for RS1, RS2, RD (all zero in padding) - for _ in 0..3 { - ops.push(BitwiseOperation::single_byte( - BitwiseOperationType::IsByte, - 0, - )); - } + // IS_BYTE[RS1, RS2] pair (both zero in padding) + ops.push(BitwiseOperation::byte_op( + BitwiseOperationType::IsByte, + 0, + 0, + )); + // IS_BYTE[RD, 0] single (zero in padding) + ops.push(BitwiseOperation::single_byte( + BitwiseOperationType::IsByte, + 0, + )); // 12 IS_BYTE lookups for ARG1/ARG2/RES byte pairs (all zero in padding) for _ in 0..12 { ops.push(BitwiseOperation::byte_op( diff --git a/prover/src/tests/cpu_tests.rs b/prover/src/tests/cpu_tests.rs index e736f5aff..fa8b76409 100644 --- a/prover/src/tests/cpu_tests.rs +++ b/prover/src/tests/cpu_tests.rs @@ -329,10 +329,11 @@ fn test_bus_interactions_count() { // - 1 SHIFT (shift operations) // - 1 BRANCH (branch/jump target calculation) // - 1 ECALL (single shared bus for HALT and COMMIT, mult = ECALL) - // - 3 IS_BYTE (register indices: RS1, RS2, RD; second argument 0) + // - 1 IS_BYTE for (RS1, RS2) paired + // - 1 IS_BYTE for (RD, 0) // - 12 IS_BYTE (ARG1/ARG2/RES byte pairs: 4 pairs × 3 arrays) - // Total: 8 + 8 + 8 + 2 + 1 + 1 + 1 + 1 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 3 + 12 = 56 - assert_eq!(interactions.len(), 56); + // Total: 8 + 8 + 8 + 2 + 1 + 1 + 1 + 1 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 12 = 55 + assert_eq!(interactions.len(), 55); } #[test] From aab7f133af62a83481a49ceaa2dd2671c5820fa8 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Fri, 17 Apr 2026 16:40:00 -0300 Subject: [PATCH 7/9] fix BusId --- prover/src/tables/types.rs | 42 ++++++++++++++-------------- prover/src/tests/prove_elfs_tests.rs | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/prover/src/tables/types.rs b/prover/src/tables/types.rs index c9b07ab44..8ef7c627d 100644 --- a/prover/src/tables/types.rs +++ b/prover/src/tables/types.rs @@ -49,7 +49,7 @@ pub enum BusId { /// Range check: value is a valid halfword [0, 2^16) IsHalfword, /// Range check: value is a 20-bit value [0, 2^20) - IsB20 = 3, + IsB20, // ========================================================================= // Bitwise operations (BITWISE table provides) @@ -149,26 +149,26 @@ impl TryFrom for BusId { match value { 0 => Ok(BusId::IsByte), 1 => Ok(BusId::IsHalfword), - 3 => Ok(BusId::IsB20), - 4 => Ok(BusId::AndByte), - 5 => Ok(BusId::OrByte), - 6 => Ok(BusId::XorByte), - 7 => Ok(BusId::Msb8), - 8 => Ok(BusId::Msb16), - 9 => Ok(BusId::Zero), - 10 => Ok(BusId::Hwsl), - 11 => Ok(BusId::Lt), - 12 => Ok(BusId::Mul), - 13 => Ok(BusId::Dvrm), - 14 => Ok(BusId::Shift), - 15 => Ok(BusId::Memw), - 16 => Ok(BusId::Load), - 17 => Ok(BusId::Memory), - 18 => Ok(BusId::Branch), - 19 => Ok(BusId::Decode), - 20 => Ok(BusId::Ecall), - 21 => Ok(BusId::CommitNextByte), - 22 => Ok(BusId::Commit), + 2 => Ok(BusId::IsB20), + 3 => Ok(BusId::AndByte), + 4 => Ok(BusId::OrByte), + 5 => Ok(BusId::XorByte), + 6 => Ok(BusId::Msb8), + 7 => Ok(BusId::Msb16), + 8 => Ok(BusId::Zero), + 9 => Ok(BusId::Hwsl), + 10 => Ok(BusId::Lt), + 11 => Ok(BusId::Mul), + 12 => Ok(BusId::Dvrm), + 13 => Ok(BusId::Shift), + 14 => Ok(BusId::Memw), + 15 => Ok(BusId::Load), + 16 => Ok(BusId::Memory), + 17 => Ok(BusId::Branch), + 18 => Ok(BusId::Decode), + 19 => Ok(BusId::Ecall), + 20 => Ok(BusId::CommitNextByte), + 21 => Ok(BusId::Commit), other => Err(other), } } diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 68cb5bbe3..c2bcf46f4 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1004,7 +1004,7 @@ fn test_debug_memory_bus_tokens() { let z: i128 = 1000; let alpha: i128 = 2; - let bus_id: i128 = 17; // BusId::Memory + let bus_id: i128 = 16; // BusId::Memory // Compute fingerprint for a token let fingerprint = From 18b84247bcd3c5ec1ee806657444b042b0d35dd1 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Tue, 21 Apr 2026 13:39:31 -0300 Subject: [PATCH 8/9] perf: batch PAGE IS_BYTE range checks (init, fini) into one interaction PAGE-C1 and PAGE-C2 were two separate IS_BYTE[init, 0] and IS_BYTE[fini, 0] interactions. Since INIT and FINI are in the same row, batch them as a single IS_BYTE[init, fini] interaction. The BITWISE table's (X, Y) lookup ensures both bytes are individually constrained to [0, 255]. Saves 1 bus interaction per PAGE row (4096 rows per page). For a program touching 20 pages, this eliminates 20 * 4096 = 81920 IS_BYTE multiplicities and removes 1 aux column per PAGE table instance. --- prover/src/tables/page.rs | 20 +++++--------------- prover/src/tables/trace_builder.rs | 9 ++------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index 05a0c8913..91bbcbf42 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -297,8 +297,7 @@ pub fn precomputed_commitment_cached(config: &PageConfig, options: &ProofOptions /// /// ## Bus Interactions /// -/// - PAGE-C1: IS_BYTE[init, 0] - sender, multiplicity 1 (range check) -/// - PAGE-C2: IS_BYTE[fini, 0] - sender, multiplicity 1 (range check) +/// - PAGE-C1+C2: IS_BYTE[init, fini] - sender, multiplicity 1 (batched range check) /// - PAGE-C3: memory[0, address, 0, init] - receiver, multiplicity -1 /// - PAGE-C4: memory[0, address, timestamp, fini] - sender, multiplicity 1 /// @@ -322,7 +321,7 @@ pub fn bus_interactions(page_base: u64) -> Vec { let address_hi = BusValue::constant(page_base_hi); vec![ - // PAGE-C1: IS_BYTE[init, 0] - range check initial value + // PAGE-C1+C2: IS_BYTE[init, fini] - range check both byte values in one interaction BusInteraction::sender( BusId::IsByte, Multiplicity::One, @@ -331,22 +330,13 @@ pub fn bus_interactions(page_base: u64) -> Vec { start_column: cols::INIT, packing: Packing::Direct, }, - BusValue::constant(0), - ], - ), - // PAGE-C2: IS_BYTE[fini, 0] - range check final value - BusInteraction::sender( - BusId::IsByte, - Multiplicity::One, - vec![ BusValue::Packed { start_column: cols::FINI, packing: Packing::Direct, }, - BusValue::constant(0), ], ), - // PAGE-C3: memory[0, address, 0, init] - receive initial token + // PAGE-C3: memory[0, address, 0, init] - receive initial memory token BusInteraction::receiver( BusId::Memory, Multiplicity::One, @@ -517,7 +507,7 @@ mod tests { #[test] fn test_bus_interactions() { let interactions = bus_interactions(0x1000); // page_base - assert_eq!(interactions.len(), 4); // C1, C2, C3, C4 + assert_eq!(interactions.len(), 3); // C1+C2 (batched IS_BYTE), C3, C4 } #[test] @@ -525,6 +515,6 @@ mod tests { // Test with high address like stack region let stack_page = STACK_TOP & !(DEFAULT_PAGE_SIZE as u64 - 1); let interactions = bus_interactions(stack_page); - assert_eq!(interactions.len(), 4); + assert_eq!(interactions.len(), 3); } } diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 08b09ff6d..7f6e9b372 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1397,15 +1397,10 @@ fn collect_bitwise_from_page(elf: &Elf, memory_state: &MemoryState) -> Vec Date: Tue, 21 Apr 2026 15:01:29 -0300 Subject: [PATCH 9/9] docs: align PAGE IS_BYTE batching docs --- prover/src/tables/page.rs | 7 ++-- prover/src/tables/trace_builder.rs | 7 ++-- prover/src/tests/prove_elfs_tests.rs | 53 ++++++++++++++-------------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index 91bbcbf42..872b8d553 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -26,10 +26,9 @@ //! //! | Tag | Bus | Signature | Multiplicity | //! |-----|-----|-----------|--------------| -//! | PAGE-C1 | IS_BYTE | `[init]` | 1 (sender) | -//! | PAGE-C2 | IS_BYTE | `[fini]` | 1 (sender) | -//! | PAGE-C3 | Memory | `[0, address, 0, init]` | -1 (receiver) | -//! | PAGE-C4 | Memory | `[0, address, timestamp, fini]` | 1 (sender) | +//! | PAGE-C1+C2 | IS_BYTE | `[init, fini]` | 1 (sender) | +//! | PAGE-C3 | Memory | `[0, address, 0, init]` | -1 (receiver) | +//! | PAGE-C4 | Memory | `[0, address, timestamp, fini]` | 1 (sender) | use std::collections::HashMap; use std::sync::OnceLock; diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 7f6e9b372..2d0fda1b0 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -1339,9 +1339,8 @@ fn collect_byte_check_ops_for_padding(num_padding_rows: usize) -> Vec Vec { @@ -1819,7 +1818,7 @@ fn build_traces( bitwise_ops.extend(collect_bitwise_from_memw_aligned(&memw_aligned_ops)); // MEMW_R sends IS_HALFWORD[timestamp_0 - old_timestamp_lo - 1] bitwise_ops.extend(collect_bitwise_from_memw_register(&memw_register_ops)); - // PAGE tables do IS_BYTE lookups for init and fini values (C1, C2) + // PAGE tables do a batched IS_BYTE[init, fini] lookup per row (C1+C2) if let Some(elf) = elf { bitwise_ops.extend(collect_bitwise_from_page(elf, memory_state)); } diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index c2bcf46f4..2581e6d99 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1382,57 +1382,58 @@ fn test_debug_memory_tokens_sb_sh() { println!("Found {} imbalanced memory tokens", imbalanced); } - // === Count IS_BYTE lookups from PAGE (C1 init + C2 fini) === + // === Count IS_BYTE lookups from PAGE (batched [init, fini] per row) === println!("\n=== IS_BYTE Lookup Counts (from PAGE tables) ==="); - let mut is_byte_from_page = [0u64; 256]; + let mut page_pair_counts: HashMap<(u8, u8), u64> = HashMap::new(); let total_page_rows: usize = traces.pages.iter().map(|p| p.num_rows()).sum(); for (page_idx, page_trace) in traces.pages.iter().enumerate() { let page_size = traces.page_configs[page_idx].page_size; for row in 0..page_trace.num_rows().min(page_size) { - let init = page_trace.main_table.get(row, page_cols::INIT).to_raw() as usize; - let fini = page_trace.main_table.get(row, page_cols::FINI).to_raw() as usize; - is_byte_from_page[init] += 1; // C1 - is_byte_from_page[fini] += 1; // C2 + let init = page_trace.main_table.get(row, page_cols::INIT).to_raw() as u8; + let fini = page_trace.main_table.get(row, page_cols::FINI).to_raw() as u8; + *page_pair_counts.entry((init, fini)).or_insert(0) += 1; } } + let page_is_byte_total: u64 = page_pair_counts.values().sum(); println!( - "Total PAGE rows: {}, Expected IS_BYTE: {} (2 per row)", - total_page_rows, - total_page_rows * 2 + "Total PAGE rows: {}, Expected IS_BYTE (1 per row): {}", + total_page_rows, total_page_rows, ); println!( - "IS_BYTE[0, 0]: {} lookups (most bytes are 0)", - is_byte_from_page[0] + "IS_BYTE[0, 0] from PAGE: {} lookups (most rows are (0,0))", + page_pair_counts.get(&(0, 0)).copied().unwrap_or(0) ); - // Check if bitwise table has matching multiplicities for IS_BYTE - // Single-byte IS_BYTE uses rows 0..255 with Y=0, MU_IS_BYTE is column 17 + // BITWISE row for IS_BYTE[X, Y] at Z=0 is X + 256*Y. We only sum + // multiplicity at the (X, Y) pairs PAGE actually touches. Other senders + // (e.g. CPU's paired IS_BYTE checks) also bump this same MU_IS_BYTE + // column and may hit the same (X, Y) rows, so this is a coarse sanity + // check (BITWISE mult >= PAGE's contribution), not an exact balance. use crate::tables::bitwise::cols as bitwise_cols; - let bitwise_is_byte_mult: u64 = (0..256usize) - .map(|byte_val| { - // IS_BYTE lookup is at row with X=byte_val, Y=0, Z=0 - // Row index = X + Y*256 + Z*256*256 = X (for Y=0, Z=0) + let bitwise_is_byte_mult_over_page_pairs: u64 = page_pair_counts + .keys() + .map(|&(x, y)| { + let row = x as usize + 256 * y as usize; traces .bitwise .main_table - .get(byte_val, bitwise_cols::MU_IS_BYTE) + .get(row, bitwise_cols::MU_IS_BYTE) .to_raw() }) .sum(); println!( - "Bitwise IS_BYTE total multiplicity: {}", - bitwise_is_byte_mult + "Bitwise IS_BYTE mult summed over PAGE (init, fini) rows: {}", + bitwise_is_byte_mult_over_page_pairs ); - - // Also count total IS_BYTE lookups expected from collect_bitwise_from_page - let is_byte_total_from_page: u64 = is_byte_from_page.iter().sum(); println!( "Total IS_BYTE lookups from PAGE (counted): {}", - is_byte_total_from_page + page_is_byte_total ); + // Note: this can be >= 0 because CPU byte-pair IS_BYTE senders may also + // hit some of the same (init, fini) rows. It should never be negative. println!( - "Difference: {} (should be 0 if PAGE IS_BYTE matches Bitwise)", - bitwise_is_byte_mult as i64 - is_byte_total_from_page as i64 + "Difference: {} (>= 0 expected; PAGE pairs may also receive from CPU)", + bitwise_is_byte_mult_over_page_pairs as i64 - page_is_byte_total as i64 ); // === Verify PAGE AIR uses correct page_base ===