Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
77bcc5e
feat(lfm): field-native recursion machine with the keccak family hosted
MauroToscano Jul 29, 2026
7d64b03
feat(lfm): big-endian field elements and the byte-level splice
MauroToscano Jul 29, 2026
dbac40c
feat(lfm): replay the continuation-epoch statement and Phase A
MauroToscano Jul 29, 2026
b36f15f
feat(stark): serialize AIR transition constraints as a build-time art…
MauroToscano Jul 29, 2026
50227c6
feat(lfm): mask the trailing half of a length-prefixed byte string
MauroToscano Jul 29, 2026
6dbc579
feat(lfm): chunk KECCAK_RND into program-pinned instances
MauroToscano Jul 29, 2026
5984afa
Merge branch 'feat/lfm-chunking' into feat/lfm
MauroToscano Jul 30, 2026
b4eca82
feat(lfm): real two-epoch continuation proof bytes as an R1f fixture
MauroToscano Jul 30, 2026
2a6f903
feat(prover): cover all 28 production AIRs, not the monolithic 25
MauroToscano Jul 30, 2026
2d4aa35
feat(lfm): read a real proof's committed roots into arena words
MauroToscano Jul 30, 2026
8231117
docs(lfm): record T=24 as measured, and verify the supplied roots are…
MauroToscano Jul 30, 2026
d2fb95c
docs(lfm): constraint-lowering design, backed by a standing op census
MauroToscano Jul 30, 2026
ef7587f
docs(lfm): revise lowering design against the machine's real cost model
MauroToscano Jul 30, 2026
058ba5e
measure(lfm): per-epoch constraint multiplier, and correct a wrong claim
MauroToscano Jul 30, 2026
3b621d0
measure(lfm): continuation-epoch constraint leg, pinned to the measur…
MauroToscano Jul 30, 2026
1414d72
measure(lfm): real continuation-epoch chunk counts, first-hand
MauroToscano Jul 30, 2026
69b3b34
docs(lfm): reorder the uniform promotion — epoch_label is the critica…
MauroToscano Jul 30, 2026
a4711c6
feat(lfm): authenticate a real Merkle opening under the production hash
MauroToscano Jul 30, 2026
7b966d0
docs(lfm): refine the uniform design, and hand off
MauroToscano Jul 30, 2026
af5ea7c
fix(lfm): the proof fixture is not reproducible — measure it, stop re…
MauroToscano Jul 30, 2026
b3042d6
docs(lfm): version the Phase R working docs as one consistent set
MauroToscano Jul 30, 2026
3dd4556
fix(lfm): correct the stale keccak column comments, and pin the widths
MauroToscano Jul 30, 2026
3b790ad
docs(lfm): status log for the R1f Merkle-opening slices
MauroToscano Jul 30, 2026
229f265
docs(lfm): correct an unverified claim about the fixture's sub-proofs
MauroToscano Jul 30, 2026
1cd76d3
feat(lfm): lower a ConstraintArtifact into LFM instructions
MauroToscano Jul 30, 2026
ea67783
feat(lfm): zerofier, quotient recombination and the composition check
MauroToscano Jul 30, 2026
ece6e43
docs(lfm): record what building the constraint leg corrected in its d…
MauroToscano Jul 30, 2026
9ac0ffc
Merge branch 'feat/lfm' into feat/lfm-constraint-emitter
MauroToscano Jul 30, 2026
31c5965
feat(lfm): emit the cross-epoch L2G commitment binding
MauroToscano Jul 30, 2026
370a584
docs(lfm): status log for R1g recon and the L2G binding slice
MauroToscano Jul 30, 2026
236c0f4
docs(lfm): the REGISTER derivation is the binding, and a gadget-sizin…
MauroToscano Jul 30, 2026
f6b3dd9
feat(lfm): reconstruct the deep-composition polynomial at a query point
MauroToscano Jul 30, 2026
2321e92
Merge branch 'feat/lfm' into feat/lfm-constraint-emitter
MauroToscano Jul 30, 2026
94acf24
docs(lfm): a degenerate parameter hides implementations from every re…
MauroToscano Jul 30, 2026
7492454
feat(lfm): emit the attestation's program id
MauroToscano Jul 30, 2026
39f8de8
docs(lfm): status log for the program-id slice
MauroToscano Jul 30, 2026
2541d1d
Merge branch 'feat/lfm-constraint-emitter' into feat/lfm
MauroToscano Jul 30, 2026
01796ea
docs(lfm): a resume point for Phase R
MauroToscano Jul 30, 2026
7bf0e15
docs(lfm): how to restart the work after a cold start
MauroToscano Jul 31, 2026
5f00361
feat(lfm): join DEEP to the Merkle authentication over one arena
MauroToscano Jul 31, 2026
ffa6633
docs(lfm): ruling — shared-commitment lever parked for e2e, recorded …
MauroToscano Jul 31, 2026
5215939
feat(lfm): derive the next epoch's REGISTER commitment from reg_fini
MauroToscano Jul 31, 2026
550b6d6
feat(lfm): a preprocessed fixture, because the group order is otherwi…
MauroToscano Jul 31, 2026
835d599
docs(lfm): the REGISTER derivation, measured — and a fourth degenerat…
MauroToscano Jul 31, 2026
69b4a91
Merge branch 'feat/lfm-register-derivation' into feat/lfm
MauroToscano Jul 31, 2026
f071742
Merge branch 'feat/lfm-deep-join' into feat/lfm
MauroToscano Jul 31, 2026
edfc5a8
docs(lfm): an assembly-obligations ledger, opened with the reg_fini w…
MauroToscano Jul 31, 2026
a6389e6
test(lfm): tamper both fixtures, and give one of them a walk worth wa…
MauroToscano Jul 31, 2026
4973f33
Merge branch 'feat/lfm' into feat/lfm-deep-join
MauroToscano Jul 31, 2026
b728043
measure(lfm): the shared-commitment collapse is 48%, not 70%
MauroToscano Jul 31, 2026
1b846ef
docs(lfm): the production FRI verify path, mapped and cited for the f…
MauroToscano Jul 31, 2026
703f742
Merge branch 'feat/lfm-deep-join' into feat/lfm
MauroToscano Jul 31, 2026
149980a
docs(lfm): the shared-commitment prediction corrected by measurement …
MauroToscano Jul 31, 2026
0f177ba
docs(lfm): resume point updated — both chaining legs done, FRI and Lo…
MauroToscano Jul 31, 2026
c6fc471
docs(lfm): the join obligation is about two-consumer values, not open…
MauroToscano Jul 31, 2026
8b8e55b
feat(lfm): hand a query's index bits to the next leg, additively
MauroToscano Jul 31, 2026
cff8cb4
Merge branch 'feat/lfm-register-derivation' into feat/lfm
MauroToscano Jul 31, 2026
620ec69
docs(lfm): method rule 7 — a relative test dies when its two sides unify
MauroToscano Jul 31, 2026
6712b81
feat(lfm): the LogUp closure, and the L it must not read twice
MauroToscano Jul 31, 2026
8ad9704
Merge branch 'feat/lfm' into feat/lfm-deep-join
MauroToscano Jul 31, 2026
94a55e1
Merge branch 'feat/lfm-deep-join' into feat/lfm
MauroToscano Jul 31, 2026
0218e32
docs(lfm): ledger — L-gap discharged, start_index binding opened, per…
MauroToscano Jul 31, 2026
5e93fe6
fix(lfm): derive the alpha powers, and guard the derivations absolutely
MauroToscano Jul 31, 2026
85f99c8
feat(lfm): the FRI fold layout, and the fixture cannot witness it
MauroToscano Jul 31, 2026
1418e0b
Merge branch 'feat/lfm-deep-join' into feat/lfm
MauroToscano Jul 31, 2026
ef50c95
docs(lfm): ledger — alpha-powers instance discharged, five two-consum…
MauroToscano Jul 31, 2026
ff3a3d6
Merge branch 'feat/lfm-register-derivation' into feat/lfm
MauroToscano Jul 31, 2026
19466bd
test(lfm): witness per-chunk accumulation, and hand the leg off
MauroToscano Jul 31, 2026
1145041
Merge branch 'feat/lfm-deep-join' into feat/lfm
MauroToscano Aug 3, 2026
2c152fc
docs(lfm): FRI leg state at agent retirement, and the resume point br…
MauroToscano Aug 3, 2026
8ccc8f0
docs(lfm): the restart section now points at wave 3, not at finished …
MauroToscano Aug 3, 2026
2c03c10
test(lfm): measure what a zero-row fixed table's bus contribution is
MauroToscano Aug 3, 2026
09f1966
docs(lfm): record the zero-row measurement in the handoff, RESUME and…
MauroToscano Aug 3, 2026
bc9f217
feat(lfm): the FRI emitter — per-layer walk, fold chain, terminal check
MauroToscano Aug 3, 2026
79cd8a3
test(lfm): the FRI structural guard was vacuous — replace it with a c…
MauroToscano Aug 3, 2026
04339f3
test(lfm): the FRI leg proves and verifies, not just executes
MauroToscano Aug 3, 2026
95861b5
docs(lfm): wave-3 status-log lines (fri-emitter slices 0-2, zerorow s…
MauroToscano Aug 3, 2026
5a246ba
Merge branch 'feat/lfm-fri-emitter' into feat/lfm
MauroToscano Aug 3, 2026
6d5f197
Merge branch 'feat/lfm-zerorow-experiment' into feat/lfm
MauroToscano Aug 3, 2026
35845e4
docs(lfm): wave 3 closed — FRI leg done, zerorow settled; wave 4 is a…
MauroToscano Aug 3, 2026
e105dea
docs(lfm): preserve the start_index research; wave-4 abort noted
MauroToscano Aug 3, 2026
61b4bfa
docs(lfm): the RESUME carries the wave-4 abort and the start_index po…
MauroToscano Aug 3, 2026
cde2e6a
feat(lfm): assemble the epoch verifier's Fiat-Shamir spine
MauroToscano Aug 3, 2026
7cf0c84
feat(lfm): hang the LogUp closure off the assembled spine
MauroToscano Aug 3, 2026
00d545e
docs(lfm): correct the wave-4 test count (195, not 196)
MauroToscano Aug 3, 2026
c161ae9
docs(lfm): wave-4 consolidation — research doc restored; wave-5 spawn…
MauroToscano Aug 7, 2026
cf800d9
docs(lfm): wave-5 respawn — keep the stashed seam API
MauroToscano Aug 4, 2026
ce1538b
feat(lfm): hang the verification legs off the assembled epoch spine
MauroToscano Aug 4, 2026
b9e8a88
test(lfm): count the composition and FRI terminal checks; falsify the…
MauroToscano Aug 4, 2026
13a7858
docs(lfm): wave-5 stopping point and the wave-6 order of work
MauroToscano Aug 4, 2026
724fa3a
docs(lfm): team-lead ruling on entry 7 (DECODE/PAGE stay arena cells,…
MauroToscano Aug 7, 2026
5a9226a
feat(lfm): source every preprocessed root, derive REGISTER, join DECO…
MauroToscano Aug 4, 2026
044382d
test(lfm): witness the OOD absorb order and the frame-step view
MauroToscano Aug 4, 2026
9ce241d
feat(lfm): width-check the register boundary; close out the assembly …
MauroToscano Aug 4, 2026
3918548
docs(lfm): amend the entry-7 ruling — PAGE condition reversed, migrat…
MauroToscano Aug 7, 2026
f49157c
test(lfm): the wrap harness and a per-chip cell census
MauroToscano Aug 4, 2026
9e0fcc7
test(lfm): the wrap run — prove the assembled epoch verifier, and pri…
MauroToscano Aug 4, 2026
fa1da70
test(lfm): reconcile the wrap against wave 6, and price the recursion
MauroToscano Aug 4, 2026
077e34d
docs(lfm): three box checks, all UTC, all busy — box left alone
MauroToscano Aug 7, 2026
afad011
docs(lfm): scope the hash matrix — two sockets, the aux collapse, the…
MauroToscano Aug 4, 2026
8ea2ec3
feat(lfm): measure the hash matrix's permutation axis without buildin…
MauroToscano Aug 4, 2026
b510191
docs(lfm): a Poseidon-original skeleton already exists in-tree, with …
MauroToscano Aug 4, 2026
9e4b79c
docs(lfm): revise the matrix on corpus data — every candidate fits th…
MauroToscano Aug 4, 2026
b050ccc
feat(lfm): Poseidon-original over Goldilocks, pinned to an external v…
MauroToscano Aug 4, 2026
9fad776
docs(lfm): specify the Poseidon chip executably, and stop short of bu…
MauroToscano Aug 4, 2026
35d407d
docs(lfm): close wave 8 — slice 1a done, slice 1b handed to wave 9
MauroToscano Aug 4, 2026
0f30868
feat(lfm): host Poseidon behind LFM_HASH via a construction-time hash…
MauroToscano Aug 6, 2026
8b85c41
test(lfm): prove the Poseidon chip, and measure the column it was bui…
MauroToscano Aug 6, 2026
fa065d6
docs(lfm): record the measured Poseidon column, and correct the RSS u…
MauroToscano Aug 6, 2026
4d0a031
docs(lfm): close wave 9 — slice 1b accepted, worktree handed back
MauroToscano Aug 6, 2026
a349a1e
feat(lfm): host PR #903's BLAKE3 chip and measure the blake column
MauroToscano Aug 6, 2026
5ebf062
docs(lfm): the blake column, the residue split, and the delegation price
MauroToscano Aug 6, 2026
d6514c1
docs(lfm): reconcile the residue split against the delegation evaluation
MauroToscano Aug 6, 2026
34709ce
docs(lfm): close wave 10 — hash matrix complete, worktree handed back
MauroToscano Aug 6, 2026
8aed545
docs(lfm): residue base discipline — the inter-base gap is BITWISE+KE…
MauroToscano Aug 7, 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
669 changes: 669 additions & 0 deletions crypto/stark/src/constraint_ir/artifact.rs

Large diffs are not rendered by default.

394 changes: 394 additions & 0 deletions crypto/stark/src/constraint_ir/artifact_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,394 @@
//! Unit tests for [`ConstraintArtifact`]: the codec, the lift back to a
//! [`ConstraintProgram`], the self-consistency and shape checks, and the
//! pre-captured supply path.
//!
//! The per-table bit-exactness sweep over all 25 production AIRs lives in the
//! prover crate (`prover/src/tests/constraint_artifact_tests.rs`) — it needs the
//! production tables. What is here is what the production tables CANNOT cover:
//!
//! - **Nonzero `end_exemptions`.** Every production constraint applies to every
//! row (`RowDomain::ALL`); nothing under `prover/src` uses
//! `RowDomain::except_last`. So the production sweep would exercise the
//! artifact's zerofier metadata only in its all-zero case, which proves
//! nothing about the field that capture actually discards. The AIR below has
//! exemptions on purpose.
//! - **Rejection.** A suite of AIRs that all validate proves the checks accept;
//! it does not prove they can reject. The falsification tests here corrupt an
//! artifact in each way `validate_self` claims to catch and assert it does.

use math::field::element::FieldElement;
use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as GoldilocksExtension;
use math::field::goldilocks::GoldilocksField;
use math::field::traits::IsField;

use super::artifact::{ArtifactError, ArtifactMeta, ConstraintArtifact};
use crate::constraints::builder::{ConstraintBuilder, ConstraintSet, RowDomain};
use crate::lookup::{AirWithBuses, AuxiliaryTraceBuildData, NullBoundaryConstraintBuilder};
use crate::proof::options::GoldilocksCubicProofOptions;
use crate::traits::AIR;

type Gl = GoldilocksField;
type Ext3 = GoldilocksExtension;

const NUM_COLS: usize = 4;

/// A constraint set whose three constraints have DIFFERENT row domains, so the
/// artifact's `end_exemptions` is non-uniform and a bug that dropped, zeroed, or
/// permuted it would be visible. No production table does this today.
struct ExemptConstraints;

impl<F: IsField, E: IsField> ConstraintSet<F, E> for ExemptConstraints {
fn eval<B: ConstraintBuilder<F, E>>(&self, b: &mut B) {
let m0 = b.main(0, 0);
let m1 = b.main(0, 1);
let m2 = b.main(0, 2);
let n0 = b.main(1, 0);
let n1 = b.main(1, 1);

// c0: every row — a degree-2 product.
b.emit_base(0, m0.clone() * m1.clone() - m2.clone());
// c1: skips the last row (reads the next row).
b.emit_base_rows(1, RowDomain::except_last(1), n0.clone() - m0.clone());
// c2: skips the last two rows.
b.emit_base_rows(2, RowDomain::except_last(2), n1 - m1);
}
}

fn options() -> crate::proof::options::ProofOptions {
GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is valid")
}

/// The exemption-bearing AIR, with no bus interactions so its constraints are
/// exactly the three above (no LogUp suffix).
fn exempt_air() -> AirWithBuses<Gl, Ext3, NullBoundaryConstraintBuilder, (), ExemptConstraints> {
AirWithBuses::new(
NUM_COLS,
AuxiliaryTraceBuildData {
interactions: vec![],
},
&options(),
1,
ExemptConstraints,
)
.with_name("EXEMPT")
}

// =============================================================================
// The zerofier metadata capture discards
// =============================================================================

#[test]
fn artifact_carries_the_end_exemptions_capture_discards() {
let air = exempt_air();
let artifact = ConstraintArtifact::capture(&air);

// The three constraints' row domains, which the ConstraintProgram alone has
// no field for.
let exemptions: Vec<u32> = artifact.meta.iter().map(|m| m.end_exemptions).collect();
assert_eq!(
exemptions,
vec![0, 1, 2],
"the artifact must preserve each constraint's row domain"
);
assert_eq!(artifact.constraints_meta(), air.constraints_meta().to_vec());

// ... and it must survive the wire.
let bytes = artifact.to_bytes().expect("serialize");
let back = ConstraintArtifact::from_bytes(&bytes).expect("deserialize");
assert_eq!(back, artifact, "artifact must round-trip exactly");
assert_eq!(
back.meta
.iter()
.map(|m| m.end_exemptions)
.collect::<Vec<_>>(),
vec![0, 1, 2]
);
}

#[test]
fn zeroed_exemptions_are_rejected_against_the_air() {
let air = exempt_air();
let mut artifact = ConstraintArtifact::capture(&air);
assert!(artifact.validate_against(&air).is_ok());

// Drop the zerofier shapes — the exact damage that makes a serialized
// program evaluate the right algebra against the wrong divisor.
for m in &mut artifact.meta {
m.end_exemptions = 0;
}
let err = artifact
.validate_against(&air)
.expect_err("an artifact with the row domains flattened must be rejected");
assert!(
matches!(
err,
ArtifactError::ShapeMismatch {
field: "constraints_meta",
..
}
),
"expected a constraints_meta mismatch, got {err:?}"
);
}

// =============================================================================
// Lift / codec
// =============================================================================

#[test]
fn lift_is_the_inverse_of_lower() {
let air = exempt_air();
let captured = air.constraint_program();
let artifact = ConstraintArtifact::capture(&air);
let lifted = artifact.program();

assert_eq!(
lifted.nodes, captured.nodes,
"nodes must survive the round trip"
);
assert_eq!(
lifted.dims, captured.dims,
"dims must survive the round trip"
);
assert_eq!(lifted.roots, captured.roots);
assert_eq!(lifted.num_base, captured.num_base);
assert_eq!(lifted.base_consts, captured.base_consts);
assert_eq!(lifted.ext_consts, captured.ext_consts);

// And through the wire, not just in memory.
let bytes = artifact.to_bytes().expect("serialize");
let lifted2 = ConstraintArtifact::from_bytes(&bytes)
.expect("deserialize")
.program();
assert_eq!(lifted2.nodes, captured.nodes);
assert_eq!(lifted2.dims, captured.dims);
assert_eq!(lifted2.base_consts, captured.base_consts);
}

#[test]
fn constants_survive_as_exact_field_values() {
// Constants go out as raw limbs and come back through `from_raw`; a
// canonicalization slip there would change a constraint's arithmetic
// silently, so pin the values rather than only their count.
let air = exempt_air();
let artifact = ConstraintArtifact::capture(&air);
let lifted = artifact.program();
for (i, (a, b)) in lifted
.base_consts
.iter()
.zip(air.constraint_program().base_consts.iter())
.enumerate()
{
assert_eq!(a, b, "base_consts[{i}] changed value across the round trip");
assert_eq!(
a.value(),
b.value(),
"base_consts[{i}] changed representation across the round trip"
);
}
}

// =============================================================================
// Falsification: every check must be able to reject
// =============================================================================

#[test]
fn validate_self_rejects_a_forward_reference() {
let air = exempt_air();
let mut artifact = ConstraintArtifact::capture(&air);
// Point the last node's operand at itself: no longer topologically ordered,
// which an interpreter would read as an uninitialized value.
let last = artifact.nodes.len() - 1;
artifact.nodes[last].a = last as u32;
let err = artifact
.validate_self()
.expect_err("a self-referential node must be rejected");
assert!(matches!(err, ArtifactError::Malformed(_)), "got {err:?}");
}

#[test]
fn validate_self_rejects_an_out_of_range_root() {
let air = exempt_air();
let mut artifact = ConstraintArtifact::capture(&air);
artifact.roots[0] = artifact.nodes.len() as u32;
assert!(matches!(
artifact.validate_self(),
Err(ArtifactError::Malformed(_))
));
}

#[test]
fn validate_self_rejects_an_out_of_range_constant() {
let air = exempt_air();
let mut artifact = ConstraintArtifact::capture(&air);
let n_base = artifact.base_consts.len() as u32;
let node = artifact
.nodes
.iter_mut()
.find(|n| n.op == super::device::OP_CONST_BASE)
.expect("the program reads at least one base constant");
node.a = n_base;
assert!(matches!(
artifact.validate_self(),
Err(ArtifactError::Malformed(_))
));
}

#[test]
fn validate_self_rejects_a_non_prefix_base_kind() {
let air = exempt_air();
let mut artifact = ConstraintArtifact::capture(&air);
// All three constraints are base-rooted here, so flipping one to Ext breaks
// the "Base entries form a prefix of length num_base" invariant that
// `num_base_from_meta` relies on.
artifact.meta[0].kind = ArtifactMeta::KIND_EXT;
assert!(matches!(
artifact.validate_self(),
Err(ArtifactError::Malformed(_))
));
}

#[test]
fn validate_self_rejects_permuted_metadata() {
let air = exempt_air();
let mut artifact = ConstraintArtifact::capture(&air);
artifact.meta.swap(0, 2);
assert!(matches!(
artifact.validate_self(),
Err(ArtifactError::Malformed(_))
));
}

#[test]
fn validate_against_rejects_a_shape_change() {
let air = exempt_air();
let mut artifact = ConstraintArtifact::capture(&air);
assert!(artifact.validate_against(&air).is_ok());

artifact.shape.main_width += 1;
let err = artifact
.validate_against(&air)
.expect_err("a width change must be rejected");
assert!(
matches!(
err,
ArtifactError::ShapeMismatch {
field: "main_width",
..
}
),
"got {err:?}"
);
}

#[test]
fn from_bytes_rejects_a_corrupt_artifact() {
// The codec must not hand back a structurally invalid artifact just because
// the bytes deserialized: `from_bytes` runs `validate_self`.
let air = exempt_air();
let mut artifact = ConstraintArtifact::capture(&air);
artifact.roots[0] = 9999;
let bytes = artifact.to_bytes().expect("serialize");
assert!(matches!(
ConstraintArtifact::from_bytes(&bytes),
Err(ArtifactError::Malformed(_))
));
}

// =============================================================================
// The pre-captured supply path (the scoped verify-path unban)
// =============================================================================

#[test]
fn precaptured_is_none_even_after_a_capture() {
let air = exempt_air();
assert!(
air.precaptured_constraint_program().is_none(),
"a freshly built AIR has no build-time program"
);

// Force a capture. This fills the AIR's OnceLock — but a captured program is
// NOT a build-time artifact, and the guest-safe accessor must keep saying so.
let _ = air.constraint_program();
assert!(
air.precaptured_constraint_program().is_none(),
"a program the AIR captured at runtime must never be reported as pre-captured; \
conflating the two would let a guest path believe capture had been avoided"
);
}

#[test]
fn supplying_a_program_short_circuits_capture() {
let program = ConstraintArtifact::capture(&exempt_air()).program();
let air = exempt_air().with_precaptured(program);

let supplied = air
.precaptured_constraint_program()
.expect("the supplied program must be visible");

// Pointer identity is the actual proof that no capture ran: a capture would
// have built a fresh program in the OnceLock and returned that instead.
assert!(
std::ptr::eq(air.constraint_program(), supplied),
"constraint_program() must hand back the supplied program itself, not a fresh capture"
);
}

#[test]
fn a_supplied_program_still_evaluates_correctly() {
// Supplying a program must not change what the AIR computes.
let program = ConstraintArtifact::capture(&exempt_air()).program();
let air = exempt_air().with_precaptured(program);
let artifact = ConstraintArtifact::capture(&air);
assert!(artifact.validate_against(&air).is_ok());
assert_eq!(
artifact.program().nodes,
exempt_air().constraint_program().nodes,
"a supplied program must be the same program the AIR would have captured"
);
}

#[test]
#[should_panic(expected = "roots")]
fn supplying_a_mismatched_program_panics() {
// A program for a different constraint count must not be installable.
let mut program = ConstraintArtifact::capture(&exempt_air()).program();
program.roots.pop();
let _ = exempt_air().with_precaptured(program);
}

// =============================================================================
// Degree bound
// =============================================================================

#[test]
fn composition_degree_multiplier_reproduces_the_bound() {
let air = exempt_air();
let artifact = ConstraintArtifact::capture(&air);
let k = artifact.shape.composition_degree_multiplier as usize;
assert!(k >= 1, "the multiplier must be positive");
for log_n in [8usize, 12, 20] {
let n = 1usize << log_n;
assert_eq!(
air.composition_poly_degree_bound(n),
k * n,
"the stored multiplier must reproduce the AIR's own bound at n=2^{log_n}"
);
}
}

/// A sanity floor on the field type used for constants, so a field swap does not
/// silently reinterpret the artifact's raw limbs.
#[test]
fn base_constants_are_goldilocks_limbs() {
let air = exempt_air();
let artifact = ConstraintArtifact::capture(&air);
for &c in &artifact.base_consts {
let fe = FieldElement::<Gl>::from_raw(c);
assert_eq!(
*fe.value(),
c,
"constant {c} is not a canonical Goldilocks limb"
);
}
}
Loading