Skip to content

Commit 9a86874

Browse files
committed
refactor(pvm): Reduce the precompile STARK relation to 10 AIRs
Merge the chunk-node and Keccak-sponge AIRs, and merge the EC point and group-store AIRs. Share their constraint evaluators while placing each component in a separate column band. Derive the ACE quotient arity from the relation, correcting it from eight chunks to four.
1 parent 6dd040c commit 9a86874

30 files changed

Lines changed: 2068 additions & 1461 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
- [BREAKING] Changed the `miden::core::crypto::dsa::ecdsa_k256_keccak` advice/signature ABI to `QX[8] || QY[8] || SIG_R[8] || SIG_S[8]` as little-endian u32 field elements. Existing 65-byte signature advice must be re-encoded as `(r, s)` limbs without a recovery byte ([#3222](https://github.com/0xMiden/miden-vm/pull/3222)).
4242
- [BREAKING] Migrated proof-bound precompiles to the deferred-DAG proof wire. `ExecutionProof` now carries a `DeferredStateWire`, proof serialization is incompatible with previous proof-bound precompile requests, and verification rehydrates the wire under the built-in `miden_precompiles::registry()` before binding the resulting deferred root to the STARK public inputs ([#3222](https://github.com/0xMiden/miden-vm/pull/3222)).
4343
- `FastProcessor` `restore_call_state()` and `restore_context()` now return `OperationError::Internal` instead of panicking on empty stacks ([#3371](https://github.com/0xMiden/miden-vm/pull/3371), fixes [#3296](https://github.com/0xMiden/miden-vm/issues/3296)).
44+
- [BREAKING] Reduced the precompile STARK relation from 12 AIRs to 10 by merging the chunk/node/sponge and EC point/group stores ([#3464](https://github.com/0xMiden/miden-vm/pull/3464)).
4445
- Bound deferred precompile STARK proofs to the generated precompile ACE relation digest ([#3344](https://github.com/0xMiden/miden-vm/pull/3344)).
4546
- [BREAKING] Split Poseidon2 permutation rows out of `ChipletsAir` into `Poseidon2PermutationAir`, and updated the recursive verifier ACE registry for three AIRs ([#3345](https://github.com/0xMiden/miden-vm/pull/3345)).
4647
- [BREAKING] Optimize periodic columns evaluation for fewer ACE gates ([#3347](https://github.com/0xMiden/miden-vm/pull/3347)).

crates/lifted-stark/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,10 @@ pub mod verifier;
7878

7979
pub use config::{GenericStarkConfig, StarkConfig};
8080
pub use debug::check_constraints;
81-
// `domain` and `order` are internal modules, but these error types surface through the public
82-
// `ProverError` / `VerifierError`, so they need a public path of their own.
83-
pub use domain::DomainError;
81+
// `domain` and `order` are internal modules. Their error types surface through the public
82+
// `ProverError` / `VerifierError`, and quotient-degree derivation is part of the relation
83+
// configuration contract, so these items need public paths of their own.
84+
pub use domain::{DomainError, log_quotient_degree};
8485
pub use order::ShapeError;
8586
pub use preprocessed::{Preprocessed, PreprocessedValidationError};
8687
pub use prover::{ProverError, ProverInstance};

crates/precompiles-prover/src/ace.rs

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,122 @@
77
88
use alloc::vec::Vec;
99

10-
use miden_ace_codegen::{AceCircuit, AceConfig, AceError, build_multi_air_ace_circuit};
11-
use miden_core::field::QuadFelt;
10+
use miden_ace_codegen::{AceCircuit, AceConfig, AceError, LayoutKind, build_multi_air_ace_circuit};
11+
use miden_core::{Felt, field::QuadFelt};
1212

13-
use crate::session::ChipletAir;
13+
use crate::session::{ChipletAir, NUM_CHIPLETS};
1414

1515
// MULTI-AIR ACE CIRCUIT
1616
// ================================================================================================
1717

18+
/// Per-AIR trace regions are padded to this width before concatenation, matching the LMCS wire
19+
/// alignment used by the commitment scheme.
20+
const LMCS_ALIGNMENT: usize = 8;
21+
22+
/// Number of quotient chunks the precompile relation commits to.
23+
///
24+
/// The lifted STARK verifier derives this quantity symbolically from the AIRs. Deriving it through
25+
/// the same implementation keeps the ACE circuit's READ layout coupled to the proof protocol.
26+
fn num_quotient_chunks() -> usize {
27+
let max_log_quotient_degree = ChipletAir::all()
28+
.iter()
29+
.map(miden_lifted_stark::log_quotient_degree::<Felt, QuadFelt, ChipletAir>)
30+
.max()
31+
.expect("the chiplet stack is non-empty");
32+
1usize << max_log_quotient_degree
33+
}
34+
35+
/// ACE codegen settings for the precompile chiplet relation.
36+
fn precompile_ace_config() -> AceConfig {
37+
AceConfig {
38+
num_quotient_chunks: num_quotient_chunks(),
39+
layout: LayoutKind::Masm,
40+
num_airs: NUM_CHIPLETS,
41+
}
42+
}
43+
1844
/// Builds the ACE circuit for the precompile chiplet multi-AIR relation.
1945
///
2046
/// The circuit uses the stable [`ChipletAir::all`] instance order as its canonical ACE fold order
2147
/// and aligns trace regions to eight base-field elements. These choices define the committed ACE
2248
/// encoding; they do not prescribe the lifted STARK proof order. The cross-chiplet LogUp identity
2349
/// is checked separately by `ChipletMultiAir::eval_external`.
24-
pub fn build_precompile_multi_air_ace_circuit(
25-
config: AceConfig,
26-
) -> Result<AceCircuit<QuadFelt>, AceError> {
27-
const LMCS_ALIGNMENT: usize = 8;
28-
50+
pub fn build_precompile_multi_air_ace_circuit() -> Result<AceCircuit<QuadFelt>, AceError> {
2951
let airs = ChipletAir::all();
3052
let proof_order: Vec<_> = (0..airs.len()).collect();
3153

32-
build_multi_air_ace_circuit(&airs, &proof_order, config, LMCS_ALIGNMENT)
54+
build_multi_air_ace_circuit::<ChipletAir>(
55+
&airs,
56+
&proof_order,
57+
precompile_ace_config(),
58+
LMCS_ALIGNMENT,
59+
)
3360
}
3461

3562
#[cfg(test)]
3663
mod tests {
37-
use miden_ace_codegen::{AceConfig, LayoutKind};
64+
use alloc::{format, string::String, vec::Vec};
65+
66+
use miden_core::{Felt, field::QuadFelt};
3867

39-
use super::build_precompile_multi_air_ace_circuit;
40-
use crate::session::NUM_CHIPLETS;
68+
use super::{build_precompile_multi_air_ace_circuit, precompile_ace_config};
69+
use crate::session::{ChipletAir, NUM_CHIPLETS};
4170

4271
#[test]
4372
fn precompile_multi_air_ace_circuit_builds() {
44-
let config = AceConfig {
45-
num_quotient_chunks: 8,
46-
layout: LayoutKind::Masm,
47-
num_airs: NUM_CHIPLETS,
48-
};
49-
50-
let circuit = build_precompile_multi_air_ace_circuit(config)
51-
.expect("precompile multi-AIR ACE circuit");
73+
let circuit =
74+
build_precompile_multi_air_ace_circuit().expect("precompile multi-AIR ACE circuit");
5275
assert_eq!(circuit.layout().counts.num_public, crate::logup::NUM_PUBLIC_VALUES);
5376
assert_eq!(circuit.layout().counts.num_aux_boundary, NUM_CHIPLETS);
5477
assert!(circuit.layout().counts.preprocessed_width >= 8);
5578
}
79+
80+
/// Pin the complete quotient-degree vector, not merely its maximum: otherwise a chiplet could
81+
/// drift between degrees while another chiplet kept the relation-wide maximum unchanged.
82+
#[test]
83+
fn quotient_chunks_match_the_symbolic_derivation() {
84+
const EXPECTED: [(&str, u8); NUM_CHIPLETS] = [
85+
("ChunkNodeSponge", 2),
86+
("Poseidon2", 2),
87+
("KeccakRound", 2),
88+
("BytePairLut", 1),
89+
("TranscriptEval", 1),
90+
("UintStoreMul", 1),
91+
("UintAdd", 1),
92+
("EcPointStoreGroups", 1),
93+
("EcGroupAdd", 1),
94+
("EcMsm", 1),
95+
];
96+
97+
let derived: Vec<(String, u8)> = ChipletAir::all()
98+
.iter()
99+
.map(|air| {
100+
(
101+
format!("{air:?}"),
102+
miden_lifted_stark::log_quotient_degree::<Felt, QuadFelt, ChipletAir>(air),
103+
)
104+
})
105+
.collect();
106+
let expected: Vec<(String, u8)> =
107+
EXPECTED.iter().map(|(name, degree)| ((*name).into(), *degree)).collect();
108+
assert_eq!(
109+
derived, expected,
110+
"a chiplet's quotient degree moved; if intended, re-mint the relation digest"
111+
);
112+
113+
let max = derived.iter().map(|(_, degree)| *degree).max().expect("non-empty stack");
114+
let expected_chunks = 1usize << max;
115+
assert_eq!(
116+
precompile_ace_config().num_quotient_chunks,
117+
expected_chunks,
118+
"the ACE circuit must read exactly the quotient chunks the proof carries"
119+
);
120+
let circuit =
121+
build_precompile_multi_air_ace_circuit().expect("precompile multi-AIR ACE circuit");
122+
assert_eq!(
123+
circuit.layout().counts.num_quotient_chunks,
124+
expected_chunks,
125+
"the built circuit must preserve the derived quotient arity"
126+
);
127+
}
56128
}

crates/precompiles-prover/src/ec/add/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,8 @@ where
829829
);
830830
// col 11: the result-membership cert provide, alone. −1 per mint
831831
// op (negative ⇒ provide), naming the fresh result `r` and its
832-
// group. Consumed by `r`'s point-store row (`EcPointStore`),
832+
// group. Consumed by `r`'s point-store row (the point band of
833+
// `EcPointStoreGroupsAir`),
833834
// discharging its on-curve obligation without the MAC trio; the
834835
// bus balances because a fresh result is minted by exactly one op.
835836
let cert_group: LB::Expr = local[CELL_GROUP].into();

crates/precompiles-prover/src/ec/groups.rs

Lines changed: 68 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ pub const COL_MULT: usize = 5;
7474
pub const NUM_MAIN_COLS: usize = 6;
7575

7676
// Aux: the single LogUp running-sum column (one fraction).
77-
const NUM_LOGUP_COLS: usize = 1;
77+
pub(crate) const NUM_LOGUP_COLS: usize = 1;
7878
const AUX_WIDTH: usize = 1;
79-
const COLUMN_SHAPE: [usize; NUM_LOGUP_COLS] = [1];
79+
pub(crate) const COLUMN_SHAPE: [usize; NUM_LOGUP_COLS] = [1];
8080

8181
// AIR
8282
// ================================================================================================
@@ -118,18 +118,7 @@ impl LiftedAir<Felt, QuadFelt> for EcGroupsAir {
118118
}
119119

120120
fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
121-
let local: [AB::Var; NUM_MAIN_COLS] = current_main(builder.main(), 0);
122-
let next: [AB::Var; NUM_MAIN_COLS] = next_main(builder.main(), 0);
123-
124-
let ptr: AB::Expr = local[COL_PTR].into();
125-
let ptr_next: AB::Expr = next[COL_PTR].into();
126-
127-
// The ungated chain: ptr = row + 1 for every prover, pads
128-
// included (they are just mult = 0 rows), so ptr → tuple is
129-
// injective by construction. The wrap edge is dropped, keeping
130-
// the cyclic last → first transition free.
131-
builder.when_transition().assert_zero(ptr_next - ptr.clone() - AB::Expr::ONE);
132-
builder.when_first_row().assert_zero(ptr - AB::Expr::ONE);
121+
eval_main(builder, 0);
133122

134123
// Phase 2: LogUp.
135124
let mut lb =
@@ -138,6 +127,25 @@ impl LiftedAir<Felt, QuadFelt> for EcGroupsAir {
138127
}
139128
}
140129

130+
/// Evaluate this component's base constraints in a main-trace column band.
131+
pub(crate) fn eval_main<AB>(builder: &mut AB, main_col_offset: usize)
132+
where
133+
AB: LiftedAirBuilder<F = Felt>,
134+
{
135+
let local: [AB::Var; NUM_MAIN_COLS] = current_main(builder.main(), main_col_offset);
136+
let next: [AB::Var; NUM_MAIN_COLS] = next_main(builder.main(), main_col_offset);
137+
138+
let ptr: AB::Expr = local[COL_PTR].into();
139+
let ptr_next: AB::Expr = next[COL_PTR].into();
140+
141+
// The ungated chain: ptr = row + 1 for every prover, pads
142+
// included (they are just mult = 0 rows), so ptr → tuple is
143+
// injective by construction. The wrap edge is dropped, keeping
144+
// the cyclic last → first transition free.
145+
builder.when_transition().assert_zero(ptr_next - ptr.clone() - AB::Expr::ONE);
146+
builder.when_first_row().assert_zero(ptr - AB::Expr::ONE);
147+
}
148+
141149
// LOOKUP AIR
142150
// ================================================================================================
143151

@@ -162,43 +170,51 @@ where
162170
}
163171

164172
fn eval(&self, builder: &mut LB) {
165-
let local: [LB::Var; NUM_MAIN_COLS] = current_main(builder.main(), 0);
166-
167-
// Pads zero the mult cell, so the provide needs no act gate.
168-
let neg_mult: LB::Expr = LB::Expr::ZERO - local[COL_MULT].into();
169-
170-
let provide_deg = Deg { v: 1, u: 1 };
171-
let col_deg = Deg { v: 1, u: 1 };
172-
173-
builder.next_column(
174-
|col| {
175-
col.group(
176-
"ec-groups",
177-
|g| {
178-
g.batch(
179-
"ec-groups-fractions",
180-
LB::Expr::ONE,
181-
|b| {
182-
b.insert(
183-
"provide-ecgroup",
184-
neg_mult,
185-
EcGroupMsg {
186-
group_ptr: local[COL_PTR].into(),
187-
a_ptr: local[COL_A_PTR].into(),
188-
b_ptr: local[COL_B_PTR].into(),
189-
bound_ptr: local[COL_BOUND_PTR].into(),
190-
scalar_bound_ptr: local[COL_SBOUND_PTR].into(),
191-
},
192-
provide_deg,
193-
);
194-
},
195-
col_deg,
196-
);
197-
},
198-
col_deg,
199-
);
200-
},
201-
col_deg,
202-
);
173+
eval_lookups(builder, 0);
203174
}
204175
}
176+
177+
/// Evaluate this component's LogUp columns in a main-trace column band.
178+
pub(crate) fn eval_lookups<LB>(builder: &mut LB, main_col_offset: usize)
179+
where
180+
LB: LookupBuilder<F = Felt>,
181+
{
182+
let local: [LB::Var; NUM_MAIN_COLS] = current_main(builder.main(), main_col_offset);
183+
184+
// Pads zero the mult cell, so the provide needs no act gate.
185+
let neg_mult: LB::Expr = LB::Expr::ZERO - local[COL_MULT].into();
186+
187+
let provide_deg = Deg { v: 1, u: 1 };
188+
let col_deg = Deg { v: 1, u: 1 };
189+
190+
builder.next_column(
191+
|col| {
192+
col.group(
193+
"ec-groups",
194+
|g| {
195+
g.batch(
196+
"ec-groups-fractions",
197+
LB::Expr::ONE,
198+
|b| {
199+
b.insert(
200+
"provide-ecgroup",
201+
neg_mult,
202+
EcGroupMsg {
203+
group_ptr: local[COL_PTR].into(),
204+
a_ptr: local[COL_A_PTR].into(),
205+
b_ptr: local[COL_B_PTR].into(),
206+
bound_ptr: local[COL_BOUND_PTR].into(),
207+
scalar_bound_ptr: local[COL_SBOUND_PTR].into(),
208+
},
209+
provide_deg,
210+
);
211+
},
212+
col_deg,
213+
);
214+
},
215+
col_deg,
216+
);
217+
},
218+
col_deg,
219+
);
220+
}

0 commit comments

Comments
 (0)