Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8e4b829
Bound MUL/DVRM multiplicities and bit-constrain SHIFT zbs and μ
nicole-graus Oct 2, 2026
7c7e62a
Bit-constrain LOAD μ
nicole-graus Oct 2, 2026
8580f42
Bound LT multiplicity
nicole-graus Oct 5, 2026
5fec4bf
Bound BRANCH multiplicity
nicole-graus Oct 5, 2026
4dd1ee5
Fix clippy lints in tests
nicole-graus Oct 5, 2026
6eea7c2
Bound table multiplicities with IS_BYTE instead of IS_HALFWORD
nicole-graus Oct 6, 2026
3a5a3f4
Force is_negative to 0 on unsigned shift rows
nicole-graus Oct 7, 2026
de098d6
Document why the multiplicity bound rides the ARE_BYTES bus
nicole-graus Oct 7, 2026
af6f14d
Bit-constrain KECCAK and KECCAK_RND multiplicities
nicole-graus Oct 7, 2026
01f8f5e
Fix stale test docs and rebalance forgery residue on ARE_BYTES
nicole-graus Oct 7, 2026
2c1335d
Bound BYTEWISE multiplicity with IS_BYTE and keep EQ multiplicity a bit
nicole-graus Oct 7, 2026
a4b8083
Guard the BYTEWISE multiplicity collector against a zero row cap
nicole-graus Oct 8, 2026
cb19d79
Treat a prover refusal as inconclusive instead of a rejection in the …
nicole-graus Oct 8, 2026
8e62f38
Range check multiplicities with ARE_BYTES sends as in spec
nicole-graus Oct 13, 2026
cc8a181
Align SHIFT and KECCAK_RND byte and bit checks with the spec
nicole-graus Oct 14, 2026
a85946e
Add missing multiplicity tests, guard zero row caps and fix stale docs
nicole-graus Oct 14, 2026
363b32c
Reference the SIGN template in SHIFT is_negative comments
nicole-graus Oct 15, 2026
d329077
Require padding-safe values in the #977 byte check exemption
nicole-graus Oct 15, 2026
b8824bd
Update the max rows width table and wrap a long SHIFT doc line
nicole-graus Oct 15, 2026
0ea6872
Share MU_MAX and the row dedup helper across the dedup tables
nicole-graus Oct 15, 2026
976c5e1
Describe the pre-fix BRANCH send in past tense
nicole-graus Oct 16, 2026
70c89a0
Merge main into fix/multiplicity-soundness
MauroToscano Oct 23, 2026
647c6b8
Correct comments left stale by the per-row multiplicity counting
MauroToscano Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/continuations_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ attack and why X blocks it.
### Design X

`MU` gates **every** L2G interaction (matches the standard table pattern —
LT/MUL/MEMW each gate all their interactions with one multiplicity column).
LT/MUL/MEMW gate their lookups with their multiplicity columns or sums of them; the
only constant-1 sends are the `ARE_BYTES` checks that bound LT's and MUL's own μ).

The crucial consequence — which we first mistook for redundancy — is that gating
the **Memory bus bookend** with `MU` forces `MU = 1` on every *touched* cell:
Expand Down
22 changes: 22 additions & 0 deletions executor/programs/asm/poc_divu_commit.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.text
.attribute 5, "rv64i2p1_m2p0"
.globl main
main:
# Compute 7 / 2 with a real DIVU, spill the quotient to the stack and
# commit it. The committed public output is the DVRM chip's quotient,
# so a proof reporting anything but 3 certifies a false division.
addi a3, zero, 7
addi a4, zero, 2
divu t1, a3, a4 # t1 = 7 / 2
addi sp, sp, -16
sd t1, 0(sp) # spill to stack
li a0, 1 # fd = 1
mv a1, sp # buf = sp
li a2, 8 # count = 8
li a7, 64 # syscall = Commit
ecall

addi sp, sp, 16
li a0, 0
li a7, 93 # syscall = Halt
ecall
23 changes: 23 additions & 0 deletions executor/programs/asm/poc_lb_commit.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.text
.attribute 5, "rv64i2p1_m2p0_zmmul1p0"
.globl main
main:
# Store the byte 0x05 (sign bit clear), read it back with a real
# sign-extending LB, spill the result and commit it. The committed public
# output is the LOAD chip's res, so a proof reporting anything but 5
# certifies a false sign extension.
addi sp, sp, -16
addi t0, zero, 5
sb t0, 8(sp)
lb t1, 8(sp) # t1 = sext(0x05) = 5
sd t1, 0(sp) # spill to stack
li a0, 1 # fd = 1
mv a1, sp # buf = sp
li a2, 8 # count = 8
li a7, 64 # syscall = Commit
ecall

addi sp, sp, 16
li a0, 0
li a7, 93 # syscall = Halt
ecall
22 changes: 22 additions & 0 deletions executor/programs/asm/poc_mul_commit.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.text
.attribute 5, "rv64i2p1_m2p0_zmmul1p0"
.globl main
main:
# Compute 3 * 5 with a real MUL, spill the product to the stack and
# commit it. The committed public output is the MUL chip's lo result,
# so a proof reporting anything but 15 certifies a false product.
addi a3, zero, 3
addi a4, zero, 5
mul t1, a3, a4 # t1 = 3 * 5
addi sp, sp, -16
sd t1, 0(sp) # spill to stack
li a0, 1 # fd = 1
mv a1, sp # buf = sp
li a2, 8 # count = 8
li a7, 64 # syscall = Commit
ecall

addi sp, sp, 16
li a0, 0
li a7, 93 # syscall = Halt
ecall
22 changes: 22 additions & 0 deletions executor/programs/asm/poc_sll_commit.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.text
.attribute 5, "rv64i2p1"
.globl main
main:
# Compute 1 << 1 with a real SLL, spill the result to the stack and
# commit it. The committed public output is the SHIFT chip's result,
# so a proof reporting anything but 2 certifies a false shift.
addi a3, zero, 1
addi a4, zero, 1
sll t1, a3, a4 # t1 = 1 << 1
addi sp, sp, -16
sd t1, 0(sp) # spill to stack
li a0, 1 # fd = 1
mv a1, sp # buf = sp
li a2, 8 # count = 8
li a7, 64 # syscall = Commit
ecall

addi sp, sp, 16
li a0, 0
li a7, 93 # syscall = Halt
ecall
23 changes: 23 additions & 0 deletions executor/programs/asm/poc_slt_commit.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.text
.attribute 5, "rv64i2p1"
.globl main
main:
# Compare 5 < 3 with a real SLT, spill the result to the stack and commit
# it. The committed public output is the LT chip's result, so a proof
# reporting anything but 0 certifies a false comparison. The spill comes
# right after the SLT so it is the first instruction to read the result.
addi sp, sp, -16
addi a3, zero, 5
addi a4, zero, 3
slt t1, a3, a4 # t1 = (5 < 3) = 0
sd t1, 0(sp) # spill to stack
li a0, 1 # fd = 1
mv a1, sp # buf = sp
li a2, 8 # count = 8
li a7, 64 # syscall = Commit
ecall

addi sp, sp, 16
li a0, 0
li a7, 93 # syscall = Halt
ecall
49 changes: 31 additions & 18 deletions prover/src/tables/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
//! - `carry[0]`, `carry[1]`: Carries from 64-bit addition
//!
//! ## Bus Interactions
//! - Sender: ARE_BYTES (×1 for `[next_pc_low[1], 0]`, spec template `IS_BYTE<next_pc_low[1]>`)
//! - Sender: ARE_BYTES (×1 for `[μ, next_pc_low[1]]`, spec `IS_BYTE<μ>` and
//! `IS_BYTE<next_pc_low[1]>` batched)
//! - Sender: BYTE_ALU[AND] (×1 for masking LSB)
//! - Sender: IS_HALFWORD (×3 for next_pc_high[0..3])
//! - Receiver: BRANCH (provides branch targets to CPU)
Expand All @@ -30,9 +31,9 @@ use stark::constraints::builder::{ConstraintBuilder, ConstraintSet};
use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing};
use stark::trace::TraceTable;

use std::collections::HashMap;

use super::types::{BusId, GoldilocksExtension, GoldilocksField, SHIFT_16, VmTable, alu_op};
use super::types::{
BusId, GoldilocksExtension, GoldilocksField, SHIFT_16, VmTable, alu_op, dedup_rows,
};

// =========================================================================
// Column indices for BRANCH table
Expand Down Expand Up @@ -152,18 +153,15 @@ impl BranchOperation {

/// Generates the BRANCH trace table from a list of operations.
///
/// Duplicate operations (same pc, offset, register, jalr) are merged into a single row
/// with their multiplicities summed. The table is then padded to the next power of 2.
/// Duplicate operations (same pc, offset, register, jalr) are merged into a
/// single row with their multiplicities summed, split at
/// [`MU_MAX`](crate::tables::types::MU_MAX) (see
/// [`dedup_rows`]). The table is then padded
/// to the next power of 2.
pub fn generate_branch_trace(
operations: &[BranchOperation],
) -> TraceTable<GoldilocksField, GoldilocksExtension> {
// Deduplicate operations: (pc, offset, register, jalr) -> multiplicity
let mut op_map: HashMap<BranchOperation, u64> = HashMap::new();
for op in operations {
*op_map.entry(op.clone()).or_insert(0) += 1;
}

let unique_ops: Vec<_> = op_map.into_iter().collect();
let unique_ops = dedup_rows(operations);
let num_rows = unique_ops.len().next_power_of_two().max(4);
let mut trace = TraceTable::new_main(
crate::tables::types::zeroed_fe_vec(num_rows * cols::NUM_COLUMNS),
Expand Down Expand Up @@ -219,23 +217,38 @@ pub fn generate_branch_trace(
/// Creates all bus interactions for the BRANCH table.
///
/// The BRANCH table:
/// - **Sends** ARE_BYTES lookup for next_pc_low[1] range check (Y=0)
/// - **Sends** ARE_BYTES[μ, next_pc_low[1]]: the μ bound and the next_pc_low[1]
/// range check, batched (both have multiplicity 1)
/// - **Sends** BYTE_ALU[AND] lookup for LSB masking
/// (next_pc_low[0] = unmasked_low_byte & 254)
/// - **Sends** IS_HALFWORD lookups for next_pc_high[0..3] range checks
/// - **Sends** IS_HALFWORD lookups for the next_pc_high[0..3] range checks
/// - **Receives** BRANCH lookups from CPU table
pub fn bus_interactions() -> Vec<BusInteraction> {
vec![
// ARE_BYTES[next_pc_low[1], 0] - range check bits 8-15
// IS_BYTE[μ], IS_BYTE[next_pc_low[1]] = ARE_BYTES[μ, next_pc_low[1]] | 1.
// Every lookup here fires with μ, so μ itself must be bounded and
// non-negative. Otherwise a μ = −1 twin of an honest row cancels it on
// the BRANCH bus while *receiving* its range lookups and absorbs an
// out-of-range IS_HALFWORD sent by any row (same as LT). Both checks
// have the spec's default `cond` (1), so they batch into one
// interaction; padding rows send ARE_BYTES[0, 0]. `next_pc_low_1` is
// only constrained packed (`low0 + 2^8·low1 + 2^16·high0`), so its byte
// check must not be weighted by μ either: with weight μ a row with μ < 0
// and `low1 = p−1` cancels its own (or another table's) bound on
// `(p−1, 0)` (`branch_carrier_poc::lt_forgery_with_branch_cycle_is_rejected`).
// See `types::MU_MAX`.
BusInteraction::sender(
BusId::AreBytes,
Multiplicity::Column(cols::MU),
Multiplicity::One,
vec![
BusValue::Packed {
start_column: cols::MU,
packing: Packing::Direct,
},
BusValue::Packed {
start_column: cols::NEXT_PC_LOW_1,
packing: Packing::Direct,
},
BusValue::constant(0),
],
),
// BYTE_ALU[next_pc_low[0]; AND, unmasked_low_byte, 254]
Expand Down
35 changes: 24 additions & 11 deletions prover/src/tables/bytewise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing};
use stark::trace::TraceTable;

use super::types::{BusId, GoldilocksExtension, GoldilocksField, VmTable, alu_op};
use super::types::{BusId, GoldilocksExtension, GoldilocksField, VmTable, alu_op, dedup_rows};

// =========================================================================
// Column indices for BYTEWISE table
Expand Down Expand Up @@ -92,19 +92,14 @@ impl BytewiseOperation {

/// Generates the BYTEWISE trace from a list of operations.
///
/// Duplicate operations are merged with summed multiplicities, then padded to
/// the next power of two (minimum 4).
/// Duplicate operations are merged with summed multiplicities, split at
/// [`MU_MAX`](crate::tables::types::MU_MAX) (see
/// [`dedup_rows`]), then padded to the next
/// power of two (minimum 4).
pub fn generate_bytewise_trace(
operations: &[BytewiseOperation],
) -> TraceTable<GoldilocksField, GoldilocksExtension> {
use std::collections::HashMap;

let mut op_map: HashMap<BytewiseOperation, u64> = HashMap::new();
for op in operations {
*op_map.entry(op.clone()).or_insert(0) += 1;
}

let unique_ops: Vec<_> = op_map.into_iter().collect();
let unique_ops = dedup_rows(operations);
let num_rows = unique_ops.len().next_power_of_two().max(4);
let mut trace = TraceTable::new_main(
crate::tables::types::zeroed_fe_vec(num_rows * cols::NUM_COLUMNS),
Expand Down Expand Up @@ -132,6 +127,7 @@ pub fn generate_bytewise_trace(

/// All bus interactions for the BYTEWISE table:
/// - **Sends** `BYTE_ALU[op, a[i], b[i]] -> res[i]` for each of the 8 bytes.
/// - **Sends** `ARE_BYTES[μ, 0]` to bound the multiplicity.
/// - **Receives** `ALU[a, b, op] -> res` (operands packed DWordBL -> 2 words).
pub fn bus_interactions() -> Vec<BusInteraction> {
let mut interactions = Vec::with_capacity(9);
Expand Down Expand Up @@ -161,6 +157,23 @@ pub fn bus_interactions() -> Vec<BusInteraction> {
));
}

// IS_BYTE[μ] = ARE_BYTES[μ, 0] | 1. Bounds μ to a byte and keeps it
// non-negative: the eight BYTE_ALU sends above are weighted by μ, so a free
// μ = −1 would invert them and let the row hold an out-of-range byte.
// Multiplicity 1 (the spec's default `cond`), so padding rows send
// ARE_BYTES[0, 0] too. See `types::MU_MAX`.
interactions.push(BusInteraction::sender(
BusId::AreBytes,
Multiplicity::One,
vec![
BusValue::Packed {
start_column: cols::MU,
packing: Packing::Direct,
},
BusValue::constant(0),
],
));

// ALU[a, b, op] -> res (receiver). a/b/res are DWordBL (8 bytes) packed
// into 2 words each, matching the CPU's DWordWL operands.
interactions.push(BusInteraction::receiver(
Expand Down
Loading
Loading