Skip to content

Commit 3796b02

Browse files
committed
fix(air): correct constraint ID ordering and range-checker batch size
Two bugs introduced in the initial addr range check implementation: 1. Constraint ID ordering (test_miden_vm_ood_evals_match failure): enforce_addr_range_check() is called from enforce_memory_constraints_all_rows immediately after the WORD_IDX constraints (offset +4..+5), so ADDR_RANGE must be at offset +6 (not +21 as originally placed). All subsequent offsets shift by +1 accordingly. MEMORY_COUNT corrected from 24 to 22. 2. Range-checker batch size (assertion failure in add_range_checks): The range checker requires exactly 2 or 4 values per call. The original code submitted a separate 3-value call [addr_lo, addr_hi, addr_hi_x4], violating the invariant. Fix: combine delta and addr into one 4-value call: range.add_range_checks(row, &[delta_lo, delta_hi, addr_lo, addr_hi]) The AIR bus is updated to a 4-way batch LogUp (removing the 5th addr_hi_x4 lookup), matching the processor's range-check submission. The addr_hi_x4 overflow guard is dropped; addr_lo/addr_hi ∈ [0, 2^16) plus the reconstruction constraint already guarantee word_addr ∈ [0, 2^32). Also update TAG_CHIPLETS_COUNT from 136 to 137 in ids.rs to account for the one new reconstruction constraint.
1 parent 5e5a8f8 commit 3796b02

File tree

4 files changed

+37
-81
lines changed

4 files changed

+37
-81
lines changed

air/src/constraints/chiplets/memory.rs

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,19 @@ use crate::{
5252
// ================================================================================================
5353

5454
pub const MEMORY_BASE_ID: usize = super::bitwise::BITWISE_BASE_ID + super::bitwise::BITWISE_COUNT;
55-
pub const MEMORY_COUNT: usize = 24;
55+
pub const MEMORY_COUNT: usize = 22;
5656
const MEMORY_BINARY_BASE_ID: usize = MEMORY_BASE_ID;
5757
const MEMORY_WORD_IDX_BASE_ID: usize = MEMORY_BASE_ID + 4;
58-
const MEMORY_FIRST_ROW_BASE_ID: usize = MEMORY_BASE_ID + 6;
59-
const MEMORY_DELTA_INV_BASE_ID: usize = MEMORY_BASE_ID + 10;
60-
const MEMORY_DELTA_TRANSITION_ID: usize = MEMORY_BASE_ID + 14;
61-
const MEMORY_SCW_FLAG_ID: usize = MEMORY_BASE_ID + 15;
62-
const MEMORY_SCW_READS_ID: usize = MEMORY_BASE_ID + 16;
63-
const MEMORY_VALUE_CONSIST_BASE_ID: usize = MEMORY_BASE_ID + 17;
64-
const MEMORY_ADDR_RANGE_BASE_ID: usize = MEMORY_BASE_ID + 21;
58+
// ADDR_RANGE is placed at +6 because enforce_addr_range_check is called from
59+
// enforce_memory_constraints_all_rows immediately after the WORD_IDX constraints.
60+
// Constraint IDs must match execution order: BINARY(4) + WORD_IDX(2) + ADDR_RANGE(1) = 7.
61+
const MEMORY_ADDR_RANGE_BASE_ID: usize = MEMORY_BASE_ID + 6;
62+
const MEMORY_FIRST_ROW_BASE_ID: usize = MEMORY_BASE_ID + 7;
63+
const MEMORY_DELTA_INV_BASE_ID: usize = MEMORY_BASE_ID + 11;
64+
const MEMORY_DELTA_TRANSITION_ID: usize = MEMORY_BASE_ID + 15;
65+
const MEMORY_SCW_FLAG_ID: usize = MEMORY_BASE_ID + 16;
66+
const MEMORY_SCW_READS_ID: usize = MEMORY_BASE_ID + 17;
67+
const MEMORY_VALUE_CONSIST_BASE_ID: usize = MEMORY_BASE_ID + 18;
6568

6669
const MEMORY_BINARY_NAMESPACE: &str = "chiplets.memory.binary";
6770
const MEMORY_WORD_IDX_NAMESPACE: &str = "chiplets.memory.word_idx.zero";
@@ -81,7 +84,7 @@ const MEMORY_DELTA_TRANSITION_NAMES: [&str; 1] = [MEMORY_DELTA_TRANSITION_NAMESP
8184
const MEMORY_SCW_FLAG_NAMES: [&str; 1] = [MEMORY_SCW_FLAG_NAMESPACE; 1];
8285
const MEMORY_SCW_READS_NAMES: [&str; 1] = [MEMORY_SCW_READS_NAMESPACE; 1];
8386
const MEMORY_VALUE_CONSIST_NAMES: [&str; 4] = [MEMORY_VALUE_CONSIST_NAMESPACE; 4];
84-
const MEMORY_ADDR_RANGE_NAMES: [&str; 3] = [MEMORY_ADDR_RANGE_NAMESPACE; 3];
87+
const MEMORY_ADDR_RANGE_NAMES: [&str; 1] = [MEMORY_ADDR_RANGE_NAMESPACE; 1];
8588

8689
const MEMORY_BINARY_TAGS: TagGroup = TagGroup {
8790
base: MEMORY_BINARY_BASE_ID,
@@ -716,32 +719,6 @@ fn enforce_addr_range_check<AB>(
716719
&mut idx,
717720
memory_flag.clone() * reconstruction,
718721
);
719-
720-
// Constraint 2: overflow guard — 4 * addr_hi < 2^16.
721-
//
722-
// This ensures that the maximum derived element address is:
723-
// word_addr * 4 + 3 = (addr_hi * 2^16 + addr_lo) * 4 + 3
724-
// < 2^16 * 2^16 * 4 (since addr_hi < 2^14 after guard)
725-
// = 2^32
726-
//
727-
// We enforce this by adding `4 * addr_hi` to the range-check bus (in
728-
// `append_range_checks`); here we only need to constrain the decomposition.
729-
// The actual range check of addr_lo, addr_hi, and 4*addr_hi is done via the
730-
// range-check chiplet bus — see `Memory::append_range_checks`.
731-
//
732-
// (Constraint count: 1 reconstruction + 0 AIR-binary checks for addr_lo/addr_hi
733-
// since those are enforced by the range-check bus, not inline AIR constraints.)
734-
// The third tag slot is reserved for future use or documentation purposes.
735-
tagged_assert_zero_integrity(
736-
builder,
737-
&MEMORY_ADDR_RANGE_TAGS,
738-
&mut idx,
739-
AB::Expr::ZERO, // placeholder — overflow guard lives in range-check bus
740-
);
741-
tagged_assert_zero_integrity(
742-
builder,
743-
&MEMORY_ADDR_RANGE_TAGS,
744-
&mut idx,
745-
AB::Expr::ZERO, // placeholder — range checks for lo/hi live in range-check bus
746-
);
722+
// Range-check bus lookups for addr_lo, addr_hi are submitted by the processor
723+
// via `Memory::append_range_checks` — no additional AIR constraints needed here.
747724
}

air/src/constraints/range/bus.rs

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,13 @@ where
8787
// Memory lookups: mv0 = alpha + chiplets[MEMORY_D0], mv1 = alpha + chiplets[MEMORY_D1]
8888
let mv0: AB::ExprEF = alpha.into() + local.chiplets[MEMORY_D0_IDX].clone().into();
8989
let mv1: AB::ExprEF = alpha.into() + local.chiplets[MEMORY_D1_IDX].clone().into();
90-
// Additional address range-check lookups: addr_lo, addr_hi, and 4*addr_hi.
90+
// Additional address range-check lookups: addr_lo, addr_hi.
9191
// These close the soundness gap where the delta-based range checks only bound
9292
// differences between consecutive addresses, not absolute address values.
93+
// The processor submits [delta_lo, delta_hi, addr_lo, addr_hi] as one 4-value
94+
// range-check call per memory row, matching the 4-way batch LogUp below.
9395
let mv_addr_lo: AB::ExprEF = alpha.into() + local.chiplets[MEMORY_ADDR_LO_IDX].clone().into();
9496
let mv_addr_hi: AB::ExprEF = alpha.into() + local.chiplets[MEMORY_ADDR_HI_IDX].clone().into();
95-
// 4 * addr_hi: enforces word_addr * 4 + 3 < 2^32 (overflow guard).
96-
let mv_addr_hi_x4: AB::ExprEF =
97-
alpha.into() + AB::Expr::from_u16(4) * local.chiplets[MEMORY_ADDR_HI_IDX].clone().into();
9897

9998
// Stack lookups: sv0-sv3 = alpha + decoder helper columns
10099
let sv0: AB::ExprEF = alpha.into() + local.decoder[STACK_LOOKUP_BASE].clone().into();
@@ -105,9 +104,8 @@ where
105104
// Range check value: alpha + range V column
106105
let range_check: AB::ExprEF = alpha.into() + local.range[RANGE_V_COL_IDX].clone().into();
107106

108-
// Combined lookup denominators
109-
let memory_lookups =
110-
mv0.clone() * mv1.clone() * mv_addr_lo.clone() * mv_addr_hi.clone() * mv_addr_hi_x4.clone();
107+
// Combined lookup denominators (4-way batch LogUp for memory)
108+
let memory_lookups = mv0.clone() * mv1.clone() * mv_addr_lo.clone() * mv_addr_hi.clone();
111109
let stack_lookups = sv0.clone() * sv1.clone() * sv2.clone() * sv3.clone();
112110
let lookups = range_check.clone() * stack_lookups.clone() * memory_lookups.clone();
113111

@@ -136,28 +134,14 @@ where
136134
let s2_term = sflag_rc_mem.clone() * sv0.clone() * sv1.clone() * sv3;
137135
let s3_term = sflag_rc_mem * sv0 * sv1 * sv2;
138136

139-
// Memory lookup removal terms (one per memory range-check value submitted per row)
140-
let m0_term: AB::ExprEF = mflag_rc_stack.clone()
141-
* mv1.clone()
142-
* mv_addr_lo.clone()
143-
* mv_addr_hi.clone()
144-
* mv_addr_hi_x4.clone();
145-
let m1_term = mflag_rc_stack.clone()
146-
* mv0.clone()
147-
* mv_addr_lo.clone()
148-
* mv_addr_hi.clone()
149-
* mv_addr_hi_x4.clone();
150-
let m_addr_lo_term = mflag_rc_stack.clone()
151-
* mv0.clone()
152-
* mv1.clone()
153-
* mv_addr_hi.clone()
154-
* mv_addr_hi_x4.clone();
155-
let m_addr_hi_term = mflag_rc_stack.clone()
156-
* mv0.clone()
157-
* mv1.clone()
158-
* mv_addr_lo.clone()
159-
* mv_addr_hi_x4.clone();
160-
let m_addr_hi_x4_term = mflag_rc_stack * mv0 * mv1 * mv_addr_lo * mv_addr_hi;
137+
// Memory lookup removal terms: one per range-check value submitted per row.
138+
// Processor submits [delta_lo, delta_hi, addr_lo, addr_hi] as a single 4-value call.
139+
let m0_term: AB::ExprEF =
140+
mflag_rc_stack.clone() * mv1.clone() * mv_addr_lo.clone() * mv_addr_hi.clone();
141+
let m1_term =
142+
mflag_rc_stack.clone() * mv0.clone() * mv_addr_lo.clone() * mv_addr_hi.clone();
143+
let m_addr_lo_term = mflag_rc_stack.clone() * mv0.clone() * mv1.clone() * mv_addr_hi.clone();
144+
let m_addr_hi_term = mflag_rc_stack * mv0 * mv1 * mv_addr_lo;
161145

162146
// Main constraint: b_next * lookups = b * lookups + rc_term - s_terms - m_terms
163147
builder.tagged(TAG_RANGE_BUS_BASE, RANGE_BUS_NAME, |builder| {
@@ -170,8 +154,7 @@ where
170154
+ m0_term
171155
+ m1_term
172156
+ m_addr_lo_term
173-
+ m_addr_hi_term
174-
+ m_addr_hi_x4_term,
157+
+ m_addr_hi_term,
175158
);
176159
});
177160
}

air/src/constraints/tagging/ids.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub const TAG_DECODER_COUNT: usize = 57;
5858
/// Base ID for the chiplets constraint group.
5959
pub const TAG_CHIPLETS_BASE: usize = TAG_DECODER_BASE + TAG_DECODER_COUNT;
6060
/// Number of chiplets constraints in this group.
61-
pub const TAG_CHIPLETS_COUNT: usize = 136;
61+
pub const TAG_CHIPLETS_COUNT: usize = 137;
6262

6363
/// Base ID for the bus boundary constraint group.
6464
/// 8 first-row (aux columns pinned to identity) + 8 last-row (aux columns bound to finals) = 16.

processor/src/trace/chiplets/memory/mod.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -275,21 +275,17 @@ impl Memory {
275275
};
276276

277277
let (delta_hi, delta_lo) = split_u32_into_u16(delta);
278-
range.add_range_checks(row, &[delta_lo, delta_hi]);
279-
280278
// Also range-check the absolute word_addr via its 16-bit limbs.
281279
//
282-
// The delta range-checks above only prove that consecutive addresses
283-
// differ by < 2^32. Without also range-checking the absolute address,
284-
// a dishonest prover could start the trace at word_addr = P − 1
285-
// (Goldilocks prime ≈ 2^64) and satisfy all delta constraints by
286-
// wrapping. See `enforce_addr_range_check` in the AIR for details.
280+
// Combining delta and addr into one 4-value call satisfies the range
281+
// checker's invariant (values.len() == 2 || values.len() == 4) and
282+
// matches the 4-way batch LogUp in the AIR bus constraint.
283+
//
284+
// addr_lo ∈ [0, 2^16) and addr_hi ∈ [0, 2^16) together with the
285+
// AIR reconstruction constraint (word_addr = addr_hi*2^16 + addr_lo)
286+
// guarantee word_addr ∈ [0, 2^32). See `enforce_addr_range_check`.
287287
let (addr_hi, addr_lo) = split_u32_into_u16(u64::from(addr));
288-
// The overflow guard (4 * addr_hi) ensures word_addr * 4 + 3 < 2^32,
289-
// i.e. every element address derived from this word fits in u32.
290-
let addr_hi_times_4 = u16::try_from((addr_hi as u32) * 4)
291-
.expect("addr_hi * 4 overflows u16; word_addr is out of valid range");
292-
range.add_range_checks(row, &[addr_lo, addr_hi, addr_hi_times_4]);
288+
range.add_range_checks(row, &[delta_lo, delta_hi, addr_lo, addr_hi]);
293289

294290
// update values for the next iteration of the loop
295291
prev_ctx = ctx;

0 commit comments

Comments
 (0)