Skip to content

Commit b612b84

Browse files
Cairo Serde
1 parent dcf2bf5 commit b612b84

10 files changed

Lines changed: 510 additions & 4 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ jobs:
9898
- name: Check verifier compiles on stable
9999
env:
100100
RUSTUP_TOOLCHAIN: ${{ env.STABLE_VERSION }}
101-
run: cargo check --workspace --exclude circuit-prover --exclude circuits-stark-verifier-examples
101+
run: cargo check --workspace --exclude circuit-prover --exclude circuit-cairo-serialize --exclude circuits-stark-verifier-examples
102102

103103
machete:
104104
runs-on: ubuntu-latest

CLAUDE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ RUSTDOCFLAGS="-Dwarnings" cargo doc --document-private-items --no-deps --all-fea
120120

121121
# Verifier stable check (must compile on stable 1.88.0)
122122
RUSTUP_TOOLCHAIN=1.88.0 cargo check --workspace \
123-
--exclude circuit-prover --exclude circuits-stark-verifier-examples
123+
--exclude circuit-prover --exclude circuit-cairo-serialize --exclude circuits-stark-verifier-examples
124124

125125
# Unused dependency check
126126
cargo machete
@@ -227,8 +227,10 @@ Before modifying ANY [SOUNDNESS-CRITICAL] or [SECURITY-CRITICAL] file:
227227
5. **AIR code generation**: Component constraint evaluators and subroutines in `circuit_air`
228228
and `cairo_air` are generated by stwo-air-infra. Manual edits to generated files will be lost.
229229

230-
6. **Stable Rust compatibility**: All crates except `circuit-prover` and
231-
`circuits-stark-verifier-examples` must compile on stable Rust (1.88.0). This is enforced by CI.
230+
6. **Stable Rust compatibility**: All crates except `circuit-prover`,
231+
`circuit-cairo-serialize` (depends on `circuit-prover` for the prover-output bridge),
232+
and `circuits-stark-verifier-examples` must compile on stable Rust (1.88.0).
233+
This is enforced by CI.
232234

233235
7. **Optimized test profile**: `circuit-prover` and `stwo` are compiled at opt-level 3 even
234236
in the test profile to keep Blake-related tests performant.

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ members = [
33
"crates/cairo_air",
44
"crates/circuits",
55
"crates/circuit_air",
6+
"crates/circuit_cairo_serialize",
67
"crates/circuit_common",
78
"crates/circuit_serialize",
89
"crates/circuit_prover",
@@ -48,6 +49,8 @@ stwo_cairo_prover = { package = "stwo-cairo-prover", git = "https://github.com/s
4849
stwo_cairo_common = { package = "stwo-cairo-common", git = "https://github.com/starkware-libs/stwo-cairo", rev = "0a5e70b7" }
4950
stwo_cairo_dev_utils = { package = "stwo-cairo-dev-utils", git = "https://github.com/starkware-libs/stwo-cairo", rev = "0a5e70b7" }
5051
stwo_cairo_utils = { package = "stwo-cairo-utils", git = "https://github.com/starkware-libs/stwo-cairo", rev = "0a5e70b7" }
52+
stwo-cairo-serialize = { git = "https://github.com/starkware-libs/stwo-cairo", rev = "0a5e70b7" }
53+
starknet-ff = "0.3"
5154

5255
# local crates
5356
circuits = { path = "crates/circuits"}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "circuit-cairo-serialize"
3+
version.workspace = true
4+
edition.workspace = true
5+
description = "Serializes circuit prover output into the Cairo verifier's input format."
6+
7+
[dependencies]
8+
cairo-air.workspace = true
9+
circuit-air.workspace = true
10+
circuit-prover.workspace = true
11+
starknet-ff.workspace = true
12+
stwo.workspace = true
13+
stwo-cairo-serialize.workspace = true
14+
15+
[dev-dependencies]
16+
circuit-common.workspace = true
17+
circuits.workspace = true
18+
num-traits.workspace = true
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//! Owned mirror structs for Cairo `CircuitClaim` and `CircuitInteractionClaim`.
2+
//!
3+
//! These mirror, field-by-field, the structs in
4+
//! `stwo-cairo/stwo_cairo_verifier/crates/circuit_air/src/claims.cairo`. The Cairo `Serde`
5+
//! derive serializes a struct by emitting each field in declaration order; the field
6+
//! order here MUST match the Cairo side exactly. Components with empty `Claim {}` on the
7+
//! Cairo side (fixed-size LOG_SIZE constants) contribute no fields.
8+
//!
9+
//! Both `CairoSerialize` and `CairoDeserialize` are derived, giving symmetric serde so
10+
//! these types can round-trip in tests.
11+
12+
use circuit_air::circuit_claim::{CircuitClaim, CircuitInteractionClaim};
13+
use circuit_air::circuit_components::ComponentList;
14+
use stwo::core::fields::qm31::QM31;
15+
use stwo_cairo_serialize::{CairoDeserialize, CairoSerialize};
16+
17+
/// Mirror of Cairo `CircuitClaim`.
18+
///
19+
/// Cairo layout:
20+
/// - `public_data: CircuitPublicData { output_values: Array<QM31> }`
21+
/// - one `log_size: u32` per variable-size component, in `ComponentList` order
22+
/// - fixed-size components have empty `Claim {}` and contribute zero felts.
23+
#[derive(Clone, Debug, PartialEq, Eq, CairoSerialize, CairoDeserialize)]
24+
pub struct CairoCircuitClaim {
25+
pub output_values: Vec<QM31>,
26+
pub eq_log_size: u32,
27+
pub qm31_ops_log_size: u32,
28+
pub blake_gate_log_size: u32,
29+
pub blake_round_log_size: u32,
30+
pub blake_g_log_size: u32,
31+
pub blake_output_log_size: u32,
32+
pub triple_xor_32_log_size: u32,
33+
pub m_31_to_u_32_log_size: u32,
34+
}
35+
36+
impl From<&CircuitClaim> for CairoCircuitClaim {
37+
fn from(c: &CircuitClaim) -> Self {
38+
let CircuitClaim { log_sizes, output_values } = c;
39+
Self {
40+
output_values: output_values.clone(),
41+
eq_log_size: log_sizes[ComponentList::Eq as usize],
42+
qm31_ops_log_size: log_sizes[ComponentList::Qm31Ops as usize],
43+
blake_gate_log_size: log_sizes[ComponentList::BlakeGate as usize],
44+
blake_round_log_size: log_sizes[ComponentList::BlakeRound as usize],
45+
blake_g_log_size: log_sizes[ComponentList::BlakeG as usize],
46+
blake_output_log_size: log_sizes[ComponentList::BlakeOutput as usize],
47+
triple_xor_32_log_size: log_sizes[ComponentList::TripleXor32 as usize],
48+
m_31_to_u_32_log_size: log_sizes[ComponentList::M31ToU32 as usize],
49+
}
50+
}
51+
}
52+
53+
/// Mirror of Cairo `CircuitInteractionClaim`.
54+
///
55+
/// Cairo layout: 16 named QM31 fields in `ComponentList` order. The Rust prover stores
56+
/// the same data as `[QM31; 16]`; this struct just gives each entry a name so the derive
57+
/// macro can produce identical felt output.
58+
#[derive(Clone, Debug, PartialEq, Eq, CairoSerialize, CairoDeserialize)]
59+
pub struct CairoCircuitInteractionClaim {
60+
pub eq: QM31,
61+
pub qm31_ops: QM31,
62+
pub blake_gate: QM31,
63+
pub blake_round: QM31,
64+
pub blake_round_sigma: QM31,
65+
pub blake_g: QM31,
66+
pub blake_output: QM31,
67+
pub triple_xor_32: QM31,
68+
pub m_31_to_u_32: QM31,
69+
pub verify_bitwise_xor_8: QM31,
70+
pub verify_bitwise_xor_12: QM31,
71+
pub verify_bitwise_xor_4: QM31,
72+
pub verify_bitwise_xor_7: QM31,
73+
pub verify_bitwise_xor_9: QM31,
74+
pub range_check_15: QM31,
75+
pub range_check_16: QM31,
76+
}
77+
78+
impl From<&CircuitInteractionClaim> for CairoCircuitInteractionClaim {
79+
fn from(c: &CircuitInteractionClaim) -> Self {
80+
let s = &c.claimed_sums;
81+
Self {
82+
eq: s[ComponentList::Eq as usize],
83+
qm31_ops: s[ComponentList::Qm31Ops as usize],
84+
blake_gate: s[ComponentList::BlakeGate as usize],
85+
blake_round: s[ComponentList::BlakeRound as usize],
86+
blake_round_sigma: s[ComponentList::BlakeRoundSigma as usize],
87+
blake_g: s[ComponentList::BlakeG as usize],
88+
blake_output: s[ComponentList::BlakeOutput as usize],
89+
triple_xor_32: s[ComponentList::TripleXor32 as usize],
90+
m_31_to_u_32: s[ComponentList::M31ToU32 as usize],
91+
verify_bitwise_xor_8: s[ComponentList::VerifyBitwiseXor8 as usize],
92+
verify_bitwise_xor_12: s[ComponentList::VerifyBitwiseXor12 as usize],
93+
verify_bitwise_xor_4: s[ComponentList::VerifyBitwiseXor4 as usize],
94+
verify_bitwise_xor_7: s[ComponentList::VerifyBitwiseXor7 as usize],
95+
verify_bitwise_xor_9: s[ComponentList::VerifyBitwiseXor9 as usize],
96+
range_check_15: s[ComponentList::RangeCheck15 as usize],
97+
range_check_16: s[ComponentList::RangeCheck16 as usize],
98+
}
99+
}
100+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! Rust mirror of the Cairo `CircuitVerifierConfig` struct.
2+
//!
3+
//! Field order MUST match
4+
//! `stwo-cairo/stwo_cairo_verifier/crates/circuit_air/src/lib.cairo::CircuitVerifierConfig`.
5+
6+
use stwo::core::vcs::blake2_hash::Blake2sHash;
7+
use stwo_cairo_serialize::{CairoDeserialize, CairoSerialize};
8+
9+
#[derive(Clone, Debug, PartialEq, Eq, CairoSerialize, CairoDeserialize)]
10+
pub struct CairoCircuitVerifierConfig {
11+
/// Variable indices of the circuit's `Output` gates. One entry per public output value.
12+
pub output_addresses: Vec<u32>,
13+
/// Number of Blake gates in the circuit.
14+
pub n_blake_gates: u32,
15+
/// Expected preprocessed-trace root.
16+
pub preprocessed_root: Blake2sHash,
17+
/// Per-column log sizes in the circuit's preprocessed trace, in canonical column order.
18+
pub preprocessed_column_log_sizes: Vec<u32>,
19+
/// `trace_log_size + log_blowup_factor`. The rust circuit prover packs this into the
20+
/// channel via `PcsConfig::mix_into` (in `stwo::core::pcs`), but cairo's `PcsConfig`
21+
/// has no such field, so the verifier needs it via out-of-band config — analogous to
22+
/// the rust in-circuit verifier reading it from `ProofConfig.fri.log_trace_size`.
23+
pub lifting_log_size: u32,
24+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! Serializes circuit prover output into the format expected by the Cairo verifier
2+
//! (`stwo-cairo/stwo_cairo_verifier/crates/circuit_verifier`).
3+
//!
4+
//! The Cairo verifier's executable signature is:
5+
//!
6+
//! ```cairo
7+
//! fn main(proof: CircuitProof, config: CircuitVerifierConfig) -> VerificationOutput
8+
//! ```
9+
//!
10+
//! and uses the standard `#[derive(Serde)]` to deserialize each argument from the
11+
//! felt252 input stream produced by `scarb execute --arguments-file`. This crate emits
12+
//! the corresponding stream from the Rust prover output.
13+
14+
pub mod claim;
15+
pub mod config;
16+
pub mod proof;
17+
18+
#[cfg(test)]
19+
mod test;
20+
21+
pub use claim::{CairoCircuitClaim, CairoCircuitInteractionClaim};
22+
pub use config::CairoCircuitVerifierConfig;
23+
pub use proof::{CairoCircuitProof, CairoStarkProofForCircuit, prepare_cairo_verifier_input};
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! Top-level Cairo verifier input.
2+
//!
3+
//! Mirrors the Cairo verifier's `main(proof: CircuitProof, config: CircuitVerifierConfig)`
4+
//! signature. Field order MUST match the Cairo `CircuitProof` struct in
5+
//! `stwo-cairo/stwo_cairo_verifier/crates/circuit_air/src/lib.cairo`.
6+
//!
7+
//! The serializer is asymmetric on `queried_values`: the prover stores them in tree-major
8+
//! order, the Cairo verifier reads them sorted by column size and transposed (see
9+
//! `cairo_air::utils::sort_and_transpose_queried_values`). The conversion in
10+
//! [`prepare_cairo_verifier_input`] applies the sort before the derive emits felts.
11+
12+
use cairo_air::utils::sort_and_transpose_queried_values;
13+
use circuit_prover::prover::CircuitProof as CircuitProverOutput;
14+
use starknet_ff::FieldElement;
15+
use stwo::core::ColumnVec;
16+
use stwo::core::pcs::TreeVec;
17+
use stwo::core::pcs::quotients::CommitmentSchemeProof;
18+
use stwo::core::proof::StarkProof;
19+
use stwo::core::vcs::blake2_hash::Blake2sHash;
20+
use stwo::core::vcs_lifted::blake2_merkle::Blake2sM31MerkleHasher;
21+
use stwo::core::vcs_lifted::merkle_hasher::MerkleHasherLifted;
22+
use stwo_cairo_serialize::{CairoDeserialize, CairoSerialize};
23+
24+
use crate::CairoCircuitVerifierConfig;
25+
use crate::claim::{CairoCircuitClaim, CairoCircuitInteractionClaim};
26+
27+
/// Owned mirror of the Cairo `CircuitProof` struct, with `queried_values` already sorted
28+
/// and transposed into the layout the Cairo verifier expects.
29+
///
30+
/// Symmetric `CairoSerialize`/`CairoDeserialize` derive — both directions read fields in
31+
/// declaration order, so this round-trips cleanly.
32+
// Note: cannot derive `PartialEq`/`Eq` because `FriProof`/`MerkleDecommitmentLifted` do
33+
// not implement them. Roundtrip tests compare serialized bytes instead.
34+
#[derive(Clone, Debug, CairoSerialize, CairoDeserialize)]
35+
pub struct CairoCircuitProof<H: MerkleHasherLifted<Hash = Blake2sHash> = Blake2sM31MerkleHasher> {
36+
pub claim: CairoCircuitClaim,
37+
pub interaction_pow: u64,
38+
pub interaction_claim: CairoCircuitInteractionClaim,
39+
pub stark_proof: CairoStarkProofForCircuit<H>,
40+
pub channel_salt: u32,
41+
}
42+
43+
/// Owned counterpart of `CommitmentSchemeProof` with `queried_values` already in the
44+
/// 2D sorted-and-transposed layout (one `Vec<BaseField>` per tree, concatenated across
45+
/// queries) that the Cairo verifier deserializes.
46+
#[derive(Clone, Debug, CairoSerialize, CairoDeserialize)]
47+
pub struct CairoStarkProofForCircuit<
48+
H: MerkleHasherLifted<Hash = Blake2sHash> = Blake2sM31MerkleHasher,
49+
> {
50+
pub config: stwo::core::pcs::PcsConfig,
51+
pub commitments: Vec<Blake2sHash>,
52+
pub sampled_values: Vec<ColumnVec<Vec<stwo::core::fields::qm31::QM31>>>,
53+
pub decommitments: Vec<stwo::core::vcs_lifted::verifier::MerkleDecommitmentLifted<H>>,
54+
/// Sorted+transposed queried values (per tree).
55+
pub queried_values: Vec<Vec<stwo::core::fields::m31::M31>>,
56+
pub proof_of_work: u64,
57+
pub fri_proof: stwo::core::fri::FriProof<H>,
58+
}
59+
60+
impl<H: MerkleHasherLifted<Hash = Blake2sHash>> CairoStarkProofForCircuit<H> {
61+
/// Builds the Cairo-ready stark proof from a Rust `StarkProof` plus per-tree column
62+
/// log sizes for the [trace, interaction] trees (used to sort `queried_values`).
63+
pub fn from_stark_proof(
64+
proof: &StarkProof<H>,
65+
trace_and_interaction_trace_log_sizes: &[&[u32]; 2],
66+
) -> Self {
67+
let CommitmentSchemeProof {
68+
config,
69+
commitments,
70+
sampled_values,
71+
decommitments,
72+
queried_values,
73+
proof_of_work,
74+
fri_proof,
75+
} = &proof.0;
76+
77+
let sorted = sort_and_transpose_queried_values(
78+
queried_values,
79+
trace_and_interaction_trace_log_sizes.to_vec(),
80+
);
81+
82+
Self {
83+
config: *config,
84+
commitments: (**commitments).clone(),
85+
sampled_values: (**sampled_values).clone(),
86+
decommitments: (**decommitments).clone(),
87+
queried_values: (*sorted).clone(),
88+
proof_of_work: *proof_of_work,
89+
fri_proof: fri_proof.clone(),
90+
}
91+
}
92+
}
93+
94+
impl<H: MerkleHasherLifted<Hash = Blake2sHash>> CairoCircuitProof<H> {
95+
/// Builds from the live circuit prover output. Errors if the prover failed.
96+
pub fn from_prover_output(
97+
prover_output: &CircuitProverOutput<H>,
98+
) -> Result<Self, &'static str> {
99+
let extended = prover_output
100+
.stark_proof
101+
.as_ref()
102+
.map_err(|_| "circuit prover failed to produce a stark proof")?;
103+
let stark_proof = &extended.proof;
104+
105+
let log_sizes = column_log_sizes_by_tree(prover_output);
106+
let stark_proof = CairoStarkProofForCircuit::<H>::from_stark_proof(
107+
stark_proof,
108+
&[log_sizes[0].as_slice(), log_sizes[1].as_slice()],
109+
);
110+
111+
Ok(Self {
112+
claim: CairoCircuitClaim::from(&prover_output.claim),
113+
interaction_pow: prover_output.interaction_pow_nonce,
114+
interaction_claim: CairoCircuitInteractionClaim::from(&prover_output.interaction_claim),
115+
stark_proof,
116+
channel_salt: prover_output.channel_salt,
117+
})
118+
}
119+
}
120+
121+
/// Builds the felt252 input stream for the Cairo circuit verifier from a live prover
122+
/// output and a verifier config.
123+
pub fn prepare_cairo_verifier_input<H: MerkleHasherLifted<Hash = Blake2sHash>>(
124+
prover_output: &CircuitProverOutput<H>,
125+
config: &CairoCircuitVerifierConfig,
126+
) -> Result<Vec<FieldElement>, &'static str> {
127+
let proof = CairoCircuitProof::<H>::from_prover_output(prover_output)?;
128+
let mut bytes = Vec::new();
129+
CairoSerialize::serialize(&proof, &mut bytes);
130+
CairoSerialize::serialize(config, &mut bytes);
131+
Ok(bytes)
132+
}
133+
134+
/// `[trace_log_sizes, interaction_log_sizes]` from the prover output's components, in
135+
/// the order in which the prover committed columns. Each component contributes
136+
/// `n_trace_columns` cells for tree 1 and `n_interaction_columns` for tree 2, all
137+
/// tagged with that component's `log_size`.
138+
fn column_log_sizes_by_tree<H: MerkleHasherLifted<Hash = Blake2sHash>>(
139+
prover_output: &CircuitProverOutput<H>,
140+
) -> [Vec<u32>; 2] {
141+
let mut trace = Vec::new();
142+
let mut interaction = Vec::new();
143+
for component in &prover_output.components {
144+
let bounds: TreeVec<ColumnVec<u32>> = component.trace_log_degree_bounds();
145+
if let Some(t) = bounds.get(1) {
146+
trace.extend_from_slice(t);
147+
}
148+
if let Some(i) = bounds.get(2) {
149+
interaction.extend_from_slice(i);
150+
}
151+
}
152+
[trace, interaction]
153+
}

0 commit comments

Comments
 (0)